Für die Parallelisierung einer seriellen Anwendung gibt es verschiedene Vorgehensweisen. Interessant ist, dass sich in diesem Bereich nur Sprachergänzungen (OpenMP, Cilk) zu den gängigen Programmiersprachen (C/C++, Fortran) und Bibliotheksansätze (PVM, MPI, Pthreads, Win32-Threads) durchgesetzt haben und nicht etwa inhärent parallele Sprache wie Erlang. Auch die Intel Threading Building Blocks (TBB) bieten eine solche Bibliothek [1].
Thread-basierte Modelle
Die meisten parallelen Ansätze beruhen auf dem Prozesskontext-Modell. Der Kontext beinhaltet den vollständigen Zustand eines Prozesses zu einem bestimmten Zeitpunkt. Prozesse können ihrerseits mehrere nebenläufige Threads erzeugen, von denen jeder einzelne seriell abläuft. Die Reihenfolge und Priorität, mit denen die CPU die einzelnen Instruktionen oder Bereiche der Threads ausführt, ist dem Betriebssystem oder einem eigenen Steuerprogramm (Scheduler) überlassen.
Die Erzeugung von Threads ist deutlich schneller und effizienter als die Erzeugung von Prozessen, weil kein vollständiger Austausch des Prozesskontextes notwendig ist. Threads teilen sich eine Reihe von Betriebsmitteln mit dem zugehörigen Prozess, etwa Code- und Datensegmente sowie Dateideskriptoren. Jeder Thread besitzt aber einen eigenen Befehlszähler und Stack. Da Threads demselben Prozess zugeordnet sind, kommunizieren sie über den gemeinsamen Adressenraum. Diese Kommunikation ist sehr schnell, da der limitierende Faktor nur die Cache-Latenz oder die Speicherzugriffszeit ist.
Zu den Shared-Memory-Programmiermodellen gehören alle Ansätze, die im weitesten Sinne auf Multithreading basieren, also Thread-Bibliotheken wie Pthreads [2] oder Win32-Threads, Sprachen mit Thread-Erweiterungen wie Java und C# und Spracherweiterungen wie OpenMP (siehe den Artikel in diesem Schwerpunkt).
Dieser Artikel stellt die für die Threading Building Blocks relevanten Teile des Shared-Memory-Modells vor. Wie alle anderen Multithreading-Varianten greifen auch sie auf Betriebssystemaufrufe zum Erzeugen und Verwalten von Threads zurück. Bei gleichzeitiger Ausführung mehrerer Prozesse oder Threads muss das Betriebssystem oder ein eigener Scheduler die Zugriffe auf die vorhandenen Ressourcen wie CPU, Speicher und Festplatte regeln.
Der Scheduling-Algorithmus entscheidet zum Beispiel, wann und in welcher Reihenfolge die Instruktionen der verschiedenen Threads relativ zueinander ablaufen und wann ein Thread eine Speicherzelle ändert. Solche Zustandsänderungen sind besonders kritisch, wenn mehrere Threads auf denselben Speicher zugreifen. In solchen Fällen kann es zu so genannten Wettläufen (Race Conditions) kommen, die in der Praxis unvorhersehbar und oft nicht reproduzierbar sind, da sie von dem jeweiligen zeitlichen Ablauf abhängen.
Schwierige Fehlersuche
Ein weiteres Problem taucht immer dann auf, wenn sich Threads durch abhängige Zugriffe auf dieselben Ressourcen blockieren (Deadlock). Diese Mechanismen bewirken bei parallelen Programmen eine Unbestimmtheit, die sequenzielle Programme nicht aufweisen. Dadurch ergeben sich Probleme, die beim Entwicklungszyklus der Software zusätzliche Werkzeuge notwendig machen, um die Korrektheit des Ablaufs zu überwachen und Speicherzugriffsfehler von Threads aufdecken zu können.
Die Multithreading-Ansätze unterscheiden sich im Wesentlichen durch Granularität und Funktionsumfang. Dabei geht es im Kern darum, ob der Entwickler die maximale Kontrolle bei der Erzeugung und Synchronisation der Threads haben und damit auch den komplexesten und fehleranfälligsten Weg gehen möchte (Win32-Threads, Pthreads) oder ob er einen einfachen, eventuell inkrementellen Weg wie OpenMP wählt oder den Template-basierten Ansatz der Threading Building Blocks. Bei OpenMP wie auch bei TBB überlässt der Programmierer Details wie Erzeugung und Synchronisation der Laufzeitumgebung, gibt dadurch aber einen Teil der Kontrolle ab.
Ein richtig synchronisiertes paralleles Programm zu entwerfen ist deutlich komplexer und anfälliger für Fehler als der Entwurf seines sequenziellen Pendants. Das liegt an der Verwendung der Betriebssystemfunktionen zur Synchronisation von Threads sowie an den Verfahren zur Zugriffssteuerung, den so genannten Monitoren, Mutexen (Mutual Exclusion), Semaphoren und Locks.
Ein Mutex-Objekt zum Beispiel verhindert, dass nebenläufige Threads gleichzeitig auf Daten zugreifen und somit inkonsistente Zustände erzeugen können. Für einen objektorientierten Ansatz wäre es wünschenswert, wenn es gekapselte Varianten für den Zugriff auf die feingranularen Betriebssystemfunktionen sowie die Zugriffsbeschränkungen gäbe, die sich nahtlos in den objektorientierten Gesamtentwurf eingliedern.