Rheinwerk Computing < openbook >

 
Inhaltsverzeichnis
1 Einleitung
2 Die Programmiersprache Python
Teil I Einstieg in Python
3 Erste Schritte im interaktiven Modus
4 Der Weg zum ersten Programm
5 Kontrollstrukturen
6 Dateien
7 Das Laufzeitmodell
8 Funktionen, Methoden und Attribute
9 Informationsquellen zu Python
Teil II Datentypen
10 Das Nichts – NoneType
11 Operatoren
12 Numerische Datentypen
13 Sequenzielle Datentypen
14 Zuordnungen
15 Mengen
16 Collections
17 Datum und Zeit
18 Aufzählungstypen – Enum
Teil III Fortgeschrittene Programmiertechniken
19 Funktionen
20 Modularisierung
21 Objektorientierung
22 Ausnahmebehandlung
23 Iteratoren
24 Kontextobjekte
25 Manipulation von Funktionen und Methoden
Teil IV Die Standardbibliothek
26 Mathematik
27 Kryptografie
28 Reguläre Ausdrücke
29 Schnittstelle zu Betriebssystem und Laufzeitumgebung
30 Kommandozeilenparameter
31 Dateisystem
32 Parallele Programmierung
33 Datenspeicherung
34 Netzwerkkommunikation
35 Debugging und Qualitätssicherung
36 Dokumentation
Teil V Weiterführende Themen
37 Anbindung an andere Programmiersprachen
38 Distribution von Python-Projekten
39 Grafische Benutzeroberflächen
40 Python als serverseitige Programmiersprache im WWW – ein Einstieg in Django
41 Wissenschaftliches Rechnen
42 Insiderwissen
43 Von Python 2 nach Python 3
A Anhang
Stichwortverzeichnis

Download:
- Beispielprogramme, ca. 464 KB

Jetzt Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Python 3 von Johannes Ernesti, Peter Kaiser
Das umfassende Handbuch
Buch: Python 3

Python 3
Pfeil 32 Parallele Programmierung
Pfeil 32.1 Prozesse, Multitasking und Threads
Pfeil 32.1.1 Die Leichtgewichte unter den Prozessen – Threads
Pfeil 32.1.2 Threads oder Prozesse?
Pfeil 32.2 Pythons Schnittstellen zur Parallelisierung
Pfeil 32.3 Parallelisierung von Funktionsaufrufen
Pfeil 32.3.1 Ein Beispiel mit einem futures.ThreadPoolExecutor
Pfeil 32.3.2 Executor-Instanzen als Kontext-Manager
Pfeil 32.3.3 Die Verwendung von futures.ProcessPoolExecutor
Pfeil 32.3.4 Die Verwaltung der Aufgaben eines Executors
Pfeil 32.4 Die Module threading und multiprocessing
Pfeil 32.5 Die Thread-Unterstützung in Python
Pfeil 32.5.1 Kritische Bereiche mit Lock-Objekten absichern
Pfeil 32.5.2 Datenaustausch zwischen Threads mit Critical Sections
Pfeil 32.5.3 Gefahren von Critical Sections – Deadlocks
Pfeil 32.6 Einblick in das Modul multiprocessing
Pfeil 32.7 Ausblick
 
Zum Seitenanfang

32.5    Die Thread-Unterstützung in Python Zur vorigen ÜberschriftZur nächsten Überschrift

Mit dem Modul threading wird eine objektorientierte Schnittstelle für die Arbeit mit Threads angeboten.

Jeder Thread ist dabei Instanz einer Klasse, die von threading.Thread erbt. Wir wollen ein Programm schreiben, das in mehreren Threads parallel prüft, ob vom Benutzer eingegebene Zahlen Primzahlen[ 132 ](Eine Primzahl ist eine natürliche Zahl, die genau zwei Teiler besitzt. Die ersten sechs Primzahlen sind demnach 2, 3, 5, 7, 11 und 13. ) sind. Zu diesem Zweck definieren wir eine Klasse PrimzahlThread, die von threading.Thread erbt und als Parameter für den Konstruktor die zu überprüfende Zahl erwartet.

Die Klasse threading.Thread besitzt eine Methode namens start, die den Thread ausführt. Was genau ausgeführt werden soll, bestimmt die run-Methode, die wir mit unserer Primzahlberechnung überschreiben. Im ersten Schritt soll der Benutzer in einer Eingabeaufforderung Zahlen eingeben können, die dann überprüft werden. Ist die Überprüfung abgeschlossen, wird das Ergebnis auf dem Bildschirm ausgegeben. Das Programm inklusive der Klasse PrimzahlThread sieht folgendermaßen aus:[ 133 ](Der verwendete Algorithmus für die Primzahlprüfung ist sehr primitiv und dient hier nur als Beispiel für eine rechenintensive Funktion. )

import threading
class PrimzahlThread(threading.Thread):
def __init__(self, zahl):
super().__init__()
self.Zahl = zahl
def run(self):
i = 2
while i*i <= self.Zahl:
if self.Zahl % i == 0:
print("{} ist nicht prim, "
"da {} = {} * {}".format( self.Zahl,
self.Zahl, i, self.Zahl // i))
return
i += 1
print("{} ist prim".format(self.Zahl))
meine_threads = []
eingabe = input("> ")
while eingabe != "e":
try:
thread = PrimzahlThread(int(eingabe))
meine_threads.append(thread)
thread.start()
except ValueError:
print("Falsche Eingabe!")
eingabe = input("> ")
for t in meine_threads:
t.join()
[»]  Hinweis

Der obige Code sollte in einer Python-Konsole ausgeführt werden, da er eine Schwachstelle enthält, die erst in Abschnitt 32.5.1 beschrieben und behoben wird. Insbesondere in Entwicklungsumgebungen wie IDLE können Darstellungsfehler bei der Ausgabe auftreten.

Innerhalb der Schleife wird die Eingabe vom Benutzer eingelesen, und es wird geprüft, ob es sich um das Schlüsselwort "e" (für »Ende«) zum Beenden des Programms handelt. Wurde etwas anderes als "e" eingegeben und lässt sich dies in eine ganze Zahl umwandeln, wird eine neue Instanz der Klasse PrimzahlThread mit der Benutzereingabe als Parameter erzeugt und mit der start-Methode gestartet.

Das Programm verwaltet außerdem eine Liste namens meine_threads, in der alle Threads gespeichert werden. Nach dem Verlassen der Eingabeschleife wird über meine_threads iteriert und für jeden Thread die join-Methode aufgerufen. Die Methode join sorgt dafür, dass das Hauptprogramm so lange wartet, bis alle gestarteten Threads beendet worden sind, denn join unterbricht die Programmausführung so lange, bis der Thread, für den es aufgerufen wurde, terminiert wurde.

Ein Programmlauf könnte dann so aussehen, wobei die teils verzögerten Ausgaben zeigen, dass tatsächlich im Hintergrund gerechnet wurde:

> 737373737373737
> 5672435793
5672435793 ist nicht prim, da 5672435793 = 3 * 1890811931
> 909091
909091 ist prim
> 10000000000037
> 5643257
5643257 ist nicht prim, da 5643257 = 23 * 245359
> 4567
4567 ist prim
10000000000037 ist prim
737373737373737 ist prim
> e
 
Zum Seitenanfang

32.5.1    Kritische Bereiche mit Lock-Objekten absichern Zur vorigen ÜberschriftZur nächsten Überschrift

Eine Schwachstelle des Programms besteht darin, dass ein Thread nach beendeter Rechnung das Ergebnis ausgeben kann, während der Benutzer die nächste Zahl zur Prüfung eingibt. Dadurch verliert der Benutzer unter Umständen die Übersicht, was er schon eingegeben hat, wie das folgende Beispiel zeigt:

> 10000000000037
> 5610000000000037 ist prim
547
56547 ist nicht prim, da 56547 = 3 * 18849
> ende

In diesem Fall hat der Benutzer die Zahl 10000000000037 auf ihre Primzahleigenschaft hin untersuchen wollen. Unglücklicherweise wurde der Thread, der die Überprüfung übernahm, genau dann fertig, als der Benutzer bereits die ersten beiden Ziffern 56 der nächsten zu prüfenden Zahl 56547 eingegeben hatte. Dies führte zu einer »Zerstückelung« der Eingabe und sollte natürlich vermieden werden.

Um solche Probleme zu vermeiden, kann ein Programm Stellen markieren, die nicht parallel in mehreren Threads laufen dürfen. Man bezeichnet solche Stellen auch als Critical Sections (dt. »kritische Abschnitte«). Critical Sections werden durch sogenannte Lock-Objekte (von engl. to lock = »sperren«) realisiert. Das Modul threading stellt die Klasse threading.Lock zur Verfügung, um solche Lock-Objekte zu erzeugen.

lock_objekt = threading.Lock()

Lock-Objekte haben die beiden Methoden acquire und release, die jeweils beim Betreten bzw. beim Verlassen einer Critical Section aufgerufen werden müssen. Wenn die acquire-Methode eines Lock-Objekts aufgerufen wurde, ist es gesperrt. Ruft ein Thread die acquire-Methode eines gesperrten Lock-Objekts auf, muss er so lange warten, bis das Lock-Objekt wieder mit release freigegeben worden ist. Diese Technik verhindert, dass eine Critical Section von mehreren Threads gleichzeitig ausgeführt wird.

Typischerweise werden Lock-Objekte in der folgenden Weise verwendet:

lock_objekt.acquire()
# Hier kommt der kritische Code
lock_objekt.release()

Um nicht jedes Mal die umschließenden Aufrufe von acquire und release schreiben zu müssen, lassen sich Lock-Objekte als Kontext-Manager mit der with-Anweisung verwenden. Der oben gezeigte Code lässt sich dann übersichtlicher in der folgenden Form schreiben:

with lock_objekt:
# Hier kommt der kritische Code

Wir können unser Beispielprogramm nun folgendermaßen um Critical Sections erweitern, wobei sowohl die Benutzereingabe als auch die Ausgaben der Threads mit demselben Lock-Objekt gesichert werden.

import threading
class PrimzahlThread(threading.Thread):
EinAusLock = threading.Lock()
def __init__(self, zahl):
super().__init__()
self.Zahl = zahl
def run(self):
i = 2
while i*i <= self.Zahl:
if self.Zahl % i == 0:
with PrimzahlThread.EinAusLock:
print("{} ist nicht prim, "
"da {} = {} * {}".format( self.Zahl,
self.Zahl, i, self.Zahl // i))
return
i += 1
with PrimzahlThread.EinAusLock:
print("{} ist prim".format(self.Zahl))
meine_threads = []
eingabe = input("> ")
while eingabe != "e":
try:
thread = PrimzahlThread(int(eingabe))
meine_threads.append(thread)
thread.start()
except ValueError:
with PrimzahlThread.EinAusLock:
print("Falsche Eingabe!")
with PrimzahlThread.EinAusLock:
eingabe = input("> ")
for t in meine_threads:
t.join()

Mit dieser Erweiterung kann es nicht mehr passieren, dass ein Thread unkontrolliert sein Ergebnis ausgibt, während der Benutzer eine Eingabe vornimmt. Wir wollen uns überlegen, wie dies genau funktioniert.

Während das Programm in der Zeile eingabe = input("> ") auf die Benutzereingabe wartet, ist das Lock-Objekt PrimzahlThread.EinAusLock gesperrt. Erreicht zu diesem Zeitpunkt einer der Threads eine Critical Section in der Methode run, ruft die with-Anweisung die Methode acquire von PrimzahlThread.EinAusLock. Da das Lock-Objekt aber gesperrt ist, blockiert dieser Aufruf, und die Methode run wird angehalten. Erst wenn der Benutzer seine Eingabe bestätigt hat und die Critical Section in der while-Schleife verlassen worden ist, wird das Lock-Objekt PrimzahlThread.EinAusLock wieder freigegeben, und die Methode run kann ihre Ausgabe auf den Bildschirm bringen.

Der folgende Abschnitt beschreibt eine besonders wichtige Anwendung von Critical Sections.

 
Zum Seitenanfang

32.5.2    Datenaustausch zwischen Threads mit Critical Sections Zur vorigen ÜberschriftZur nächsten Überschrift

Threads haben gegenüber Prozessen den Vorteil, dass sie sich dieselben globalen Variablen teilen und deshalb einfach Daten austauschen können. Insbesondere eignen sich Klassenattribute der jeweiligen Thread-Klasse oder globale Variablen für den Datenaustausch.

Trotzdem gibt es ein paar Stolperfallen, die Sie beim Zugriff auf dieselbe Variable durch mehrere Threads beachten müssen.[ 134 ](Die eigentliche Kunst bei der parallelen Programmierung ist es, diese Stolperfallen zu umgehen. Es ist oft schwierig, die Abläufe in parallelen Programmen zu überblicken, weswegen sich leicht Fehler einschleichen. )

Um die Problematik zu verdeutlichen, betrachten wir ein einfaches Beispiel, bei dem wir zwei Threads starten, die jeweils 2000000-mal die Zahl 1 zu einem gemeinsamen Zähler addieren.

import threading

class MeinThread(threading.Thread):
zaehler = 0
def run(self):
for i in range(2000000):
MeinThread.zaehler += 1

A = MeinThread()
B = MeinThread()
A.start(), B.start()
A.join(), B.join()

print(MeinThread.zaehler)

Der gemeinsame Zähler ist als Klassenattribut der Klasse MeinThread realisiert. Nachdem wir die Threads A und B gestartet und mit ihrer Methode join jeweils gewartet haben, bis ihre Arbeit abgeschlossen ist, geben wir den Wert des gemeinsamen Zählers aus. Da insgesamt zweimal 2000000 Additionen durchgeführt worden sind, erwarten wir als Ausgabe den Wert 4000000. Überraschenderweise gibt das Programm aber folgenden Wert aus:

3542419

Es ist sogar so, dass anscheinend jeder Aufruf des Programms einen anderen Wert ausgibt. In zehn Durchläufen auf unserem Testrechner wurden verschiedene Werte im Bereich von 2894816 bis 3235044 ausgegeben.

Bevor wir dieses Problem in unserem Programm beheben, untersuchen wir, wie es überhaupt zu diesem Verhalten kommt.

Ein Beispiel einer Race Condition

Wie Sie in der Einleitung gelernt haben, wird Nebenläufigkeit in modernen Betriebssystemen dadurch erreicht, dass Threads[ 135 ](Wir beziehen uns hier der Übersichtlichkeit halber nur auf Threads. Die beschriebene Problematik betrifft aber genauso Prozesse, wenn sie auf gemeinsamen Daten arbeiten. ) kleine Zeitfenster eingeräumt werden, um ihre Arbeit zu verrichten. Endet ein solches Zeitfenster, wird der aktuelle Zustand des Threads gespeichert, und die Kontrolle wird an den nächsten Thread weitergereicht.

Es kommt nun vor, dass das Zeitfenster eines Threads genau während der Veränderung von MeinThread.zaehler endet, denn das Erhöhen des Wertes besteht intern aus mehreren Schritten. Zuerst muss der Wert von MeinThread.zaehler gelesen werden, dann muss eine neue Instanz mit dem um eins vergrößerten bzw. verringerten Wert erzeugt werden, die im letzten Schritt mit der Referenz MeinThread.zaehler verknüpft wird.

Wenn beispielsweise der Thread A beim Erhöhen von MeinThread.zaehler vor der Erzeugung der neuen Instanz schlafen gelegt wird, kann der Thread B aktiviert werden, der ebenfalls MeinThread.zaehler erhöhen möchte. Weil aber Thread A seinen neuen Wert von MeinThread.zaehler noch nicht berechnet und auch nicht mit der Referenz verknüpft hat, liest der neu aktivierte Thread B den alten Wert von MeinThread.zaehler und erhöht diesen. Wird dann später der Thread A wieder aktiv, erhöht er den schon vorher eingelesenen Wert um eins und weist ihn MeinThread.zaehler zu. Im Ergebnis ist der Wert von MeinThread.zaehler nur um eins erhöht worden, obwohl beide Threads jeweils eine Addition durchgeführt haben.

Tabelle 32.5 veranschaulicht das beschriebene Szenario im Detail.

Z Thread A Thread B
1 Wert von MeinThread.zaehler einlesen, beispielsweise 2 schläft
Zeitfenster von A endet, und B wird aktiviert.
2 schläft Wert von MeinThread.zaehler einlesen, in diesem Fall ebenfalls 2.
Den Wert um 1 erhöhen. Im Speicher existiert nun eine neue Instanz mit dem Wert 3.
Die neue Instanz an die Referenz MeinThread.zaehler knüpfen. Damit verweist MeinThread.zaehler auf den Wert 3.
Zeitfenster von B endet, und A wird aktiviert.
3 Den Wert um 1 erhöhen. Im Speicher existiert nun eine neue Instanz mit dem Wert 3.
Die neue Instanz an die Referenz MeinThread.zaehler knüpfen. Damit verweist MeinThread.zaehler auf den Wert 3.
schläft

Tabelle 32.5    Problemszenario beim gleichzeitigen Zugriff auf eine gemeinsame Variable. Die Spalte Z zählt die Zeitfenster, die das Betriebssystem vorgibt.

Dieses Szenario erklärt, warum das Endergebnis kleiner als die erwarteten 200000 ausfallen kann. Wie oft dieses Problem in einem Lauf des Programms auftritt, hängt davon ab, wie genau das Betriebssystem die Zeitfenster der beiden Threads wählt, was wiederum von dem System selbst, den ansonsten laufenden Programmen und weiteren, nicht kalkulierbaren Bedingungen abhängt. Man bezeichnet eine solche Konstellation als Race Condition (engl. Wettlauf ).

Machen Sie sich klar, dass diese Unzuverlässigkeit unser Programm komplett wertlos macht, da das Ergebnis in dramatischer Weise von Umständen abhängt, die wir weder vorhersehen noch beeinflussen können.

Um das Problem zu lösen, müssen wir verhindern, dass einer der Threads mit der Anpassung des Wertes von MeinThread.zaehler beginnt, wenn der andere noch nicht damit fertig ist. Wie im vorangegangenen Abschnitt können wir dies dadurch erreichen, dass wir die Zeile MeinThread.zaehler += 1 als Critical Section mit einem Lock-Objekt absichern.

Dazu ändern wir die Klasse MeinThread folgendermaßen ab:

class MeinThread(threading.Thread):
lock = threading.Lock()
zaehler = 0
def run(self):
for i in range(100000):
with MeinThread.lock:
MeinThread.zaehler += 1

Nach dieser Anpassung liefert unser Programm in jedem Durchlauf den gleichen erwarteten Wert von 200000.

Tabelle 32.6 zeigt im Detail, wie die Critical Section das Problem behebt.

Z Thread A Thread B
1 Erreicht den with-Block.
Sperren des Lock-Objekts mit acquire durch with.
Wert von MeinThread.zaehler einlesen, beispielsweise 2
schläft
Zeitfenster von A endet, und B wird aktiviert.
2 schläft Erreicht den with-Block.
Die Methode acquire wird durch with aufgerufen, aber das Lock-Objekt ist bereits gesperrt. Deshalb wird B schlafen gelegt.
B wurde durch acquire schlafen gelegt. A wird weiter ausgeführt.
3 Den Wert um 1 erhöhen. Im Speicher existiert nun eine neue Instanz mit dem Wert 3.
Die neue Instanz an die Referenz MeinThread.zaehler knüpfen. Damit verweist MeinThread.zaehler auf den Wert 3.
Das Lock-Objekt wird mit release nach Verlassen des with-Blocks freigegeben.
schläft
Zeitfenster von A endet, und B wird aktiviert.
4 schläft Das Lock-Objekt wird automatisch gesperrt, da in B die Methode acquire durch with aufgerufen wurde.
Wert von MeinThread.zaehler einlesen, in diesem Fall 3.
Den Wert um 1 erhöhen. Im Speicher existiert nun eine neue Instanz mit dem Wert 4.
Die neue Instanz an die Referenz MeinThread.zaehler knüpfen. Damit verweist MeinThread.zaehler auf den Wert 4.
Das Lock-Objekt wird mit release nach Verlassen des with-Blocks wieder freigegeben.

Tabelle 32.6    Lösung des MeinThread.zaehler-Problems mit einem Lock-Objekt

Sie sollten darauf achten, dass Sie in Ihren eigenen Programmen alle Stellen, in denen Probleme durch Zugriffe von mehreren Threads vorkommen können, durch Critical Sections schützen.

Unzureichend abgesicherte Programme mit mehreren Threads können schwer reproduzierbare und lokalisierbare Fehler enthalten. Eine Herausforderung bei der parallelen Programmierung besteht deshalb darin, solche Probleme zu umgehen.

 
Zum Seitenanfang

32.5.3    Gefahren von Critical Sections – Deadlocks Zur vorigen ÜberschriftZur nächsten Überschrift

Wenn Sie mehrere Lock-Objekte verwenden, kann es passieren, dass das Programm in einen Zustand gerät, den es nicht mehr verlassen kann, weil zwei gelockte Threads gegenseitig aufeinander warten. Dies wird Deadlock genannt.

Das folgende Ablaufprotokoll zeigt, wie ein Deadlock entstehen kann. Dabei sind die Threads A und B zwei Threads und M und L zwei Lock-Objekte.

Z Thread A Thread B
1 Das Lock-Objekt L mit L.acquire sperren schläft
Zeitfenster von A endet, und B wird aktiviert.
2 schläft Mit M.acquire wird das Lock-Objekt M gesperrt.
Zeitfenster von B endet, und A wird aktiviert.
3 M.acquire wird gerufen. Da M bereits gesperrt ist, wird A schlafen gelegt. schläft
A wurde durch M.acquire schlafen gelegt. B wird weiter ausgeführt.
4 schläft Ruft L.acquire, woraufhin B schlafen gelegt wird, da L bereits gesperrt ist.
A wurde durch M.aquire und B durch L.aquire gesperrt.
5 schläft schläft

Tabelle 32.7    Beispielszenario eines Deadlocks

Am Ende dieses Ablaufs befinden sich beide Threads im Schlafzustand und warten auf die Freigabe eines Lock-Objekts. Da aber der jeweils andere Thread das Lock-Objekt gesperrt hat, auf dessen Freigabe gewartet wird, werden die Threads nie aufgeweckt. Der Programmablauf hängt also in diesem Zustand fest – ein Deadlock hat sich eingestellt.

Zum Abschluss dieses Kapitels geben wir Ihnen jetzt noch einen kurzen Einblick in die Verwendung des Moduls multiprocessing.

 


Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Python 3 Python 3
Jetzt Buch bestellen

 Buchempfehlungen
Zum Rheinwerk-Shop: Einstieg in Python
Einstieg in Python


Zum Rheinwerk-Shop: Python. Der Grundkurs
Python. Der Grundkurs


Zum Rheinwerk-Shop: Algorithmen mit Python
Algorithmen mit Python


Zum Rheinwerk-Shop: Objektorientierte Programmierung
Objektorientierte Programmierung


Zum Rheinwerk-Shop: Raspberry Pi. Das umfassende Handbuch
Raspberry Pi. Das umfassende Handbuch


Zum Rheinwerk-Shop: Roboter-Autos mit dem Raspberry Pi
Roboter-Autos mit dem Raspberry Pi


Zum Rheinwerk-Shop: Neuronale Netze programmieren mit Python
Neuronale Netze programmieren mit Python


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2020
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern