
Betrachten wir die Entwicklung von Prozessoren in den letzten Jahren, fällt vor allem eines auf: Die Taktfrequenzen sind auf einem ähnlichen Level geblieben, aber die Anzahl der Prozessorkerne hat sich erhöht. Die Hersteller versprechen sich – und uns – davon mehr Performance pro verbrauchtes Watt.
Was bedeutet das für den Entwickler, der die Kapazitäten solcher Multi-Core-Prozessoren ausreizen möchte? Sollte er möglichst viel parallelisieren, oder gibt es dabei auch Nachteile?
Das Ikea-Dilemma
Wer schon einmal versucht hat, mit mehreren Personen einen Schrank aufzubauen, kennt das: In der Theorie lassen sich Seiten, Türen, Rückwand etc. parallel bearbeiten. In der Praxis reicht man die Anleitung aber ständig hin und her: Team „Linke Seite“ sucht dann Schrauben, die in der Schraubentüte von Team „Rechte Seite“ stecken, der Schraubenzieher wird vom Team „Rechte Seite“ gebraucht, während er von Team „Rückseite“ in Beschlag genommen wird usw. usf.
Anleitung und Schraubentüte sind hierbei also Ressourcen, die einen synchronisierten Zugriff erfordern: Es kann immer nur eine Person bzw. ein Prozess darauf zugreifen. Erheben mehrere Personen gleichzeitig Anspruch darauf, erhält trotzdem nur eine Person den Zugriff, die anderen müssen warten. Die Alternative wäre ein Streit, in dessen Folge die Anleitung zerfetzt am Boden läge und sich die Schrauben wild im Raum verteilten. Das Projekt Schrankaufbau wäre damit erheblich gefährdet, wenn nicht sogar gescheitert.
Synchronisierung und ihre Gefahren
Auch in der Parallelen Programmierung stößt man immer wieder auf Ressourcen, die von mehreren Prozessen verwendet werden sollen. Werden sie nicht synchronisiert, kommt es auch hier zum „Streit“. Das Opfer ist dann nicht die Anleitung, sondern die Datenkonsistenz: Greifen zum Beispiel zwei Prozesse auf denselben Speicherbereich zu, kann es zu Problemen, so genannten Race Condition, kommen. Im Schlimmsten Fall entstehen ungültige Zustände, die bis zum Programmabsturz führen können.
Um solche Szenarien zu verhindern, helfen dem Programmierer Synchronisationsstrukturen wie etwa ein Mutex (Mutual Exclusion). Ein solcher Mutex stellt sicher, dass immer nur ein Prozess in einem Programmabschnitt arbeitet. Dafür braucht es allerdings zusätzliche Programmkonstrukte, die recht laufzeitintensiv sind.
Mutexe bergen außerdem auch Gefahren: Blockiert z. B. ein Prozess den Mutex A und wartet auf den Mutex B, während ein anderer Prozess Mutex B blockiert, weil er auf Mutex A wartet, spricht man von einem Deadlock, also einer Blockade, die niemals aufgelöst werden kann. Der Entwickler muss deshalb aufpassen, dass die Mutexbereiche immer in der gleichen Reihenfolge durchlaufen werden. Ein solch einfacher Deadlock ist zwar ärgerlich, aber oft schnell gefunden und behoben; Mit steigender Komplexität aber steigen auch die Deadlockmöglichkeiten über mehrere Prozesse oder ganze Systeme hinweg und werden dann sehr schwer nachzuvollziehen.
Gute Organisation ist entscheidend
Die Kunst der Parallelen Programmierung ist es also, den Kommunikationsaufwand zwischen den Prozessen zu optimieren. In unserem Schrankaufbauprojekt haben wir das unbewusst schon getan: Wir haben das Gesamtproblem „Schrank“ in Einzelprobleme („Seiten“, „Türen“ etc.) zerlegt und, sofern wir im Vorhinein die benötigten Schrauben aufgeteilt, genügend Werkzeuge bereitgestellt und die Aufbauanleitungen kopiert haben um Streit zu vermeiden, die Einzelaufgaben parallel gelöst und zum Schluss die Ergebnisse dieser Aufgaben in das Gesamtergebnis integriert, also zuerst aus Seiten, Rücken, Boden und Oberteil den Korpus montiert, dann die Türen, Einlegebretter, Schubkästen etc. eingebaut. In der Informatik nennt man das „Divide and conquer“, also„Teile und herrsche“.
Der Vorteil dieses Vorgehens ist, dass die Einzelprobleme für sich alleine lösbar sind, sich ggf. sogar weiter aufteilen lassen. Eine Verteilung der Aufgaben funktioniert dadurch sehr gut, sogar über Rechnergrenzen hinweg. Deshalb wird das Verfahren auch bei Clustern und Supercomputern eingesetzt.
Zum Vergleich: Ein Mutex benötigt bei jedem Ein- und Austritt die Kommunikation mit allen Beteiligten. Die Kommunikationsgeschwindigkeit zwischen zwei Rechnern ist aber deutlich niedriger als zwischen den Rechenkernen eines Prozessors. Dazu kommt, dass man mehr CPU-Kerne und damit mehr Beteiligte hat. Ein Mutex skaliert also deutlich schlechter. Leider ist aber nicht jedes Problem so leicht zu zerlegen.
... sowohl bei der Möbelmontage
Denken wir nun etwas größer: Wir eröffnen unseren eigenen Schrankaufbauservice. Wir starten mit 16 Monteuren. Wie verteilen wir hier die Arbeit? Alle 16 Monteure als geschlossene Gruppe zu allen Auftraggebern schicken und die Schränke dort jeweils „parallel“ aufbauen? Immerhin ist dies die schnellste Art, einen Schrank zu errichten.
Oder doch besser die Monteure einzeln bzw. in kleineren Teams losschicken? Damit würde der Aufbau des einzelnen Schranks zwar ein paar Minuten länger dauern, aber wir könnten mehrere Kunden gleichzeitig bedienen – der Kommunikationsaufwand wäre zudem lediglich darauf beschränkt, festzulegen, wer zu welchem Kunden fährt. Eine geschickte Verteilung sorgte ferner für weniger Fahrtzeit und Fahrtkosten. Für unseren Schrankaufbauservice wäre dieses Vorgehen also deutlich effektiver.

... als auch in der Parallelen Programmierung
Ähnlich verhält es sich mit Applikations- und Webservern, die viele Anfragen abarbeiten müssen: Der Applikationsserver verwaltet die Verbindungen, Sitzungen etc. in verschiedenen Prozessen. Er stellt aber sicher, dass eine Sitzung niemals in mehreren Prozessen verarbeitet wird. Somit kann der Applikationsserver mehrere Anfragen mit einem Mal abarbeiten, während der Applikationsentwickler seine Logik linear programmieren und so auf die Komplexität von Mutex, Race Conditions & Co verzichten kann. Die Gesamtanwendung arbeitet auf diese Weise effektiver, als wenn jede Anfrage mit allen verfügbaren Prozessorkernen bearbeitet werden würde. Ein weiterer positiver Nebeneffekt: Stellt jemand eine zeitaufwendige Anfrage, werden andere Anfragen davon nicht aufgehalten oder gar ganz blockiert.
Fazit
Parallele Programmierung hat also viel Potential, birgt aber auch viel Risiko. Ein erfahrener Entwickler muss entscheiden, wo und wieviel Parallelisierung gut ist. Trifft man die richtigen Entscheidungen, kann man viel an Zeit und Effizienz gewinnen.
Lesen Sie hier unseren Blogbeitrag zu den mathematischen Hintergründen der Parallelen Programmierung.