32 Parallele Programmierung 

Dieses Kapitel wird Sie in die parallele Programmierung mit Python einführen, wodurch es möglich wird, mehrere Aufgaben gleichzeitig auszuführen. Bevor wir mit den technischen Details und Beispielprogrammen beginnen können, erläutern wir Ihnen einige Begriffe, und wir betrachten die prinzipielle Arbeitsweise moderner Betriebssysteme.
32.1 Prozesse, Multitasking und Threads 

Im Folgenden werden die Begriffe Programm und Prozess synonym für ein laufendes Programm verwendet.
Wir sind es als Benutzer moderner Computer gewohnt, dass mehrere Programme gleichzeitig ausgeführt werden können. Beispielsweise schreiben wir eine E-Mail, während im Hintergrund das letzte Urlaubsvideo in ein anderes Format umgewandelt wird und eine MP3-Software unseren Lieblingssong aus den Computerlautsprechern ertönen lässt. Abbildung 32.1 zeigt eine typische Arbeitssitzung, wobei jeder Kasten für ein laufendes Programm steht. Die Länge der Kästen entlang der Zeitachse zeigt an, wie lange der jeweilige Prozess läuft.
Abbildung 32.1 Mehrere Prozesse laufen gleichzeitig ab.
Faktisch kann ein Prozessor aber nur genau eine Aufgabe zu einem bestimmten Zeitpunkt übernehmen und nicht mehrere gleichzeitig. Selbst bei modernen Prozessoren mit mehr als einem Kern oder bei Rechnern mit vielen Prozessoren ist die Anzahl der gleichzeitig ausführbaren Programme durch die Anzahl der Kerne bzw. Prozessoren beschränkt. Wie ist es also möglich, dass das einleitend beschriebene Szenario auch auf einem Computer mit nur einem Prozessor, der nur einen einzigen Kern besitzt, funktioniert?
Der dahinterstehende Trick ist im Grunde sehr einfach, denn man versteckt die Limitierung der Maschine geschickt vor dem Benutzer, indem man ihm nur vorgaukelt, es würden mehrere Programme simultan laufen. Dies wird dadurch erreicht, dass man den Programmen in Zeitscheiben abwechselnd für kurze Zeit die Kontrolle über einen Prozessor zuteilt. Nach Ablauf einer solchen Zeitscheibe wird dem Programm die Kontrolle wieder entzogen, wobei sein aktueller Zustand gespeichert wird. Nun kann dem nächsten Programm eine Zeitscheibe zugeteilt werden. In der Zeit, in der ein Programm darauf wartet, eine Zeitscheibe zugeteilt zu bekommen, wird es als schlafend bezeichnet.
Sie können sich die Arbeit eines Computers so vorstellen, dass in rasender Geschwindigkeit alle laufenden Programme geweckt, für eine kurze Zeit ausgeführt und dann wieder schlafen gelegt werden. Durch die hohe Geschwindigkeit des Umschaltens zwischen den Prozessen nimmt der Benutzer dies nicht wahr. Die Verwaltung der verschiedenen Prozesse und die Zuteilung der Zeitscheiben wird vom Betriebssystem übernommen, das deshalb auch Multitasking-System (dt. »Mehrprozessbetriebssystem«) genannt wird.
Die korrekte Darstellung unseres anfänglichen Beispiels müsste also eher wie Abbildung 32.2 aussehen. Dabei symbolisiert jedes kleine Kästchen eine Zeitscheibe:
Abbildung 32.2 Die Prozesse wechseln sich ab und laufen nicht gleichzeitig.
32.1.1 Die Leichtgewichte unter den Prozessen – Threads 

Innerhalb eines Prozesses selbst kann nur eine Aufgabe zur selben Zeit ausgeführt werden, wenn das Programm sequenziell abgearbeitet wird. In vielen Situationen ist es aber von Vorteil, dass ein Programm mehrere Operationen zeitgleich durchführt. Beispielsweise darf die Benutzeroberfläche während einer aufwendigen Berechnung nicht blockieren, sondern soll den aktuellen Status anzeigen, und der Benutzer muss die Möglichkeit haben, die Berechnung gegebenenfalls abbrechen zu können. Ein anderes Beispiel ist ein Webserver, der während der Verarbeitung einer Client-Anfrage auch für weitere Zugriffe verfügbar sein muss.
Es ist möglich, die Beschränkung auf nur eine Operation zur selben Zeit dadurch zu umgehen, dass weitere Prozesse erzeugt werden. Allerdings ist die Erzeugung eines Prozesses ressourcenaufwendig, und auch der Datenaustausch zwischen den Prozessen muss geregelt werden, weil jeder Prozess seine eigenen Variablen hat, die von den anderen Prozessen abgeschirmt sind.
Eine weitere Möglichkeit, um ein Programm zu parallelisieren, bieten sogenannte Threads. Ein Thread (dt. »Faden«) ist ein Ausführungsstrang innerhalb eines Prozesses. Standardmäßig besitzt jeder Prozess genau einen Thread, der die Ausführung des Prozesses organisiert.
Nun kann ein Prozess aber auch mehrere Threads starten, die dann durch das Betriebssystem wie Prozesse scheinbar gleichzeitig ausgeführt werden. Der Vorteil von Threads gegenüber Prozessen besteht darin, dass sich die Threads eines Prozesses denselben Speicherbereich für globale Variablen teilen. Wenn also in einem Thread eine globale Variable verändert wird, ist der neue Wert auch sofort für alle anderen Threads des Prozesses sichtbar[ 127 ](Um Fehler zu vermeiden, müssen solche Zugriffe in mehreren Threads speziell mit sogenannten Critical Sections abgesichert werden. Wir werden diese Thematik später in Abschnitt 32.5.1 noch ausführlicher behandeln. ). Außerdem ist die Verwaltung von Threads für das Betriebssystem weniger aufwendig als die Verwaltung von Prozessen. Deshalb werden Threads auch Leichtgewichtprozesse genannt.
Die Threads in einem Prozess können Sie sich vorstellen wie in Abbildung 32.3.
Abbildung 32.3 Ein Prozess mit drei Threads
In CPython[ 128 ](Mit CPython wird die Referenzimplementierung von Python bezeichnet. Eine Übersicht über andere Python-Varianten finden Sie in Abschnitt 37.4 und Abschnitt 35.6.9. ) gibt es aus technischen Gründen derzeit leider keine Möglichkeit, verschiedene Threads auf verschiedenen Prozessoren oder Prozessorkernen auszuführen. Dies hat zur Folge, dass selbst Python-Programme, die intensiv auf Threading setzen, nur einen einzigen Prozessor oder Prozessorkern nutzen können.
32.1.2 Threads oder Prozesse? 

Ob Sie für Ihr Programm Threads oder Prozesse für die Parallelisierung verwenden sollten, hängt von Ihren Anforderungen ab. Wenn Sie beispielsweise ein Programm entwickeln, das eine langwierige Rechnung im Hintergrund durchführt, während die Oberfläche des Programms weiterhin verzögerungsfrei auf Benutzereingaben reagieren soll, sind Threads eine gute Wahl. Ebenso eignen sich Threads dafür, um mit langsamen Ein-/Ausgabeschnittstellen wie Netzwerkverbindungen zu arbeiten: Während ein Thread auf neue Daten wartet, kann ein anderer bereits erhaltene Daten verarbeiten.
Anders sieht es aus, wenn Ihr Programm große Datenmengen verarbeiten oder komplexe Rechnungen durchführen muss, sodass Sie gerne die gesamte verfügbare Rechenleistung mehrerer Prozessorkerne verwenden möchten. In diesem Fall sind Sie aufgrund der oben erwähnten Beschränkung von CPython gezwungen, Prozesse zu verwenden.