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.3    Parallelisierung von Funktionsaufrufen Zur vorigen ÜberschriftZur nächsten Überschrift

Für den Anwendungsfall, dass ein Programm komplexe, aber voneinander unabhängige Aufgaben ausführen muss, existiert das Modul concurrent.futures. Mit concurrent.futures können Funktionsaufrufe in komfortabler Weise in Threads oder Prozessen ausgeführt werden.

Es werden die Klassen ThreadPoolExecutor und ProcessPoolExecutor bereitgestellt, deren Instanzen jeweils eine Menge von Threads beziehungsweise Prozessen repräsentieren. Beide Klassen implementieren eine gemeinsame Schnittstelle, die eines Executors. Ein Executor dient dazu, eine Folge von Aufgaben abzuarbeiten, wobei er sich mehrerer Workers, eben der Threads beziehungsweise Prozesse, bedienen kann.

Die Schnittstelle eines Executors umfasst die folgenden Methoden:

Methode Beschreibung
submit(fn, [*args], {**kwargs}) Meldet eine neue Aufgabe beim Executor an. Die Aufgabe besteht darin, die Funktion fn mit den Parametern args und kwargs aufzurufen.
map(fn, [*iterables], {timeout}) Wendet die Funktion fn auf alle Elemente der iterierbaren Objekte iterables an und erzeugt einen Iterator über die Ergebnisse.
shutdown([wait]) Weist den Executor an, alle verwendeten Ressourcen wieder freizugeben, sobald die übergebenen Aufgaben abgearbeitet sind.

Tabelle 32.2    Die Methoden der Executor-Klassen

Sie können sich einen Executor wie den Chef eines Teams aus mehreren Arbeitern vorstellen, dem Sie über die Methode submit die Aufgaben mitteilen, die das Team für uns erledigen soll. Die Verteilung dieser Aufgaben an die einzelnen Arbeiter wird dabei vom Executor übernommen.

Beim Erzeugen einer neuen Executor-Instanz können wir die Anzahl der gewünschten Workers über den Schlüsselwortparameter max_workers festlegen.

 
Zum Seitenanfang

32.3.1    Ein Beispiel mit einem futures.ThreadPoolExecutor Zur vorigen ÜberschriftZur nächsten Überschrift

Im folgenden Beispiel verwenden wir einen ThreadPoolExecutor, um vier Aufrufe einer einfachen Beispielfunktion in drei Threads auszuführen.

from concurrent import futures
from time import sleep, time

def test(t):
sleep(t)
print("Ich habe {} Sekunden gewartet. Zeit: {:.0f}".format(t, time()))

e = futures.ThreadPoolExecutor(max_workers=3)
print("Startzeit: {:.0f}".format(time()))
e.submit(test, 9)
e.submit(test, 2)
e.submit(test, 5)
e.submit(test, 6)
print("Alle Aufgaben gestartet.")
e.shutdown()
print("Alle Aufgaben erledigt.")

Die Funktion test erzeugt eine verzögerte Ausgabe auf dem Bildschirm, wobei mit dem Parameter t die Länge der Verzögerung festgelegt werden kann. Im Beispiel lassen wir den Executor diese Funktion mit verschiedenen Verzögerungen aufrufen, was die folgende Ausgabe erzeugt:

Startzeit:                          1428830988
Alle Aufgaben gestartet.
Ich habe 2 Sekunden gewartet. Zeit: 1428830990
Ich habe 5 Sekunden gewartet. Zeit: 1428830993
Ich habe 6 Sekunden gewartet. Zeit: 1428830996
Ich habe 9 Sekunden gewartet. Zeit: 1428830997
Alle Aufgaben erledigt.

Die Zeitstempel der Ausgaben belegen, dass die Funktionsaufrufe parallel ausgeführt worden sind, denn die Ausgaben für die Wartezeiten von 2, 5 und 9 Sekunden erfolgen genau um die jeweilige Wartezeit verzögert nach der Startzeit. Wären die Aufrufe sequenziell abgearbeitet worden, hätten sich die Zeiten aufsummiert.

Eine Ausnahme bildet der Aufruf für die Wartezeit von 6 Sekunden, die erst nach 8 Sekunden auf dem Bildschirm erscheint. Der Grund für diese Verzögerung ist die Anzahl der Worker-Threads unseres Executors. Der Executor verwaltet intern eine Warteschlange der Aufgaben, die ihm per submit übergeben worden sind. Diese Warteschlange wird in derselben Reihenfolge abgearbeitet, in der die jeweiligen Aufrufe von submit erfolgt sind. In unserem Beispiel stehen dem Executor drei Threads zur Verfügung, um die vier Aufgaben zu erledigen. Zuerst wird also der Aufruf der Funktion test mit dem Parameter 9 in einem Thread gestartet. Nachdem dann auch noch die Aufrufe mit den Parametern 2 und 5 in eigenen Threads gestartet worden sind, hat der Executor die von uns festgelegte maximale Anzahl von Workers erreicht. Deshalb erfolgt der Aufruf mit dem Parameter 6 erst dann, wenn wieder ein Worker-Thread frei geworden ist. Nach 2 Sekunden ist die Aufgabe mit der kürzesten Wartezeit abgearbeitet, sodass der Aufruf für den Parameter 6 mit der entsprechenden Verzögerung gestartet wird.

Interessant ist außerdem das unterschiedliche Verhalten der Methoden submit und shutdown, denn jeder Aufruf von submit gibt die Kontrolle sofort wieder an die aufrufende Ebene zurück, ohne darauf zu warten, dass die übergebene Funktion ihre Arbeit vollendet hat. Deshalb erscheint die Ausgabe »Alle Aufgaben gestartet.« vor den Ausgaben der Funktion test. Der Aufruf von shutdown hingegen blockiert die Ausführung der aufrufenden Ebene so lange, bis alle Aufgaben abgearbeitet sind, und gibt dann die verwendeten Ressourcen wieder frei.

Wird für den Parameter wait der Wert False übergeben, gibt auch shutdown die Kontrolle sofort wieder an die aufrufende Ebene zurück, sodass dort ohne Verzögerung weitergearbeitet werden kann. Das gesamte Programm wird dann trotzdem so lange nicht beendet, wie noch Arbeiten im Hintergrund ausgeführt werden. Wichtig ist dabei, dass ein Executor nach dem Aufruf von shutdown keine weiteren Aufgaben mehr entgegennehmen kann.

Ersetzen wir im Beispielprogramm die Zeile e.shutdown() durch e.shutdown(False), ändert sich die Ausgabe deshalb folgendermaßen:

Startzeit:                          1428829782
Alle Aufgaben gestartet.
Alle Aufgaben erledigt.
Ich habe 2 Sekunden gewartet. Zeit: 1428829784
Ich habe 5 Sekunden gewartet. Zeit: 1428829787
Ich habe 6 Sekunden gewartet. Zeit: 1428829788
Ich habe 9 Sekunden gewartet. Zeit: 1428829791

Wie Sie sehen, erscheint die Ausgabe nach dem Aufruf von e.shutdown nun sofort auf dem Bildschirm, obwohl noch Hintergrundarbeiten laufen. Trotzdem wartet Python auf die noch laufenden Hintergrundprozesse, sodass noch alle verbleibenden Ausgaben erfolgen, bevor das Programm beendet wird.

 
Zum Seitenanfang

32.3.2    Executor-Instanzen als Kontext-Manager Zur vorigen ÜberschriftZur nächsten Überschrift

Der typische Lebenszyklus einer Executor-Instanz sieht so aus, dass sie erzeugt wird, ihr Aufgaben zugeteilt werden und sie dann auf das Ende der Aufgaben wartet, um abschließend wieder freigegeben zu werden.

Um diesen typischen Ablauf abzubilden, können Sie jeden Executor als Kontext-Manager mit der with-Anweisung verwenden, wodurch der explizite Aufruf von shutdown entfällt. Der letzte Teil in unserem Beispiel kann durch den folgenden with-Block ersetzt werden:

print("Startzeit:                          {:.0f}".format(time()))
with futures.ThreadPoolExecutor(max_workers=3) as e:
e.submit(test, 9)
e.submit(test, 2)
e.submit(test, 5)
e.submit(test, 6)
print("Alle Aufgaben gestartet.")
print("Alle Aufgaben erledigt.")

Wenn Sie einen Executor als Kontext-Manager mit with verwenden, wird der with-Block erst dann verlassen, wenn alle zugeteilten Aufgaben erledigt sind, da die Methode e.shutdown implizit ohne Parameter gerufen wird.

 
Zum Seitenanfang

32.3.3    Die Verwendung von futures.ProcessPoolExecutor Zur vorigen ÜberschriftZur nächsten Überschrift

Um anstelle von Threads im obigen Beispiel Prozesse für die Parallelisierung zu verwenden, wird die Klasse futures.ThreadPoolExecutor durch futures.ProcessPoolExecutor ersetzt. Außerdem muss dafür gesorgt werden, dass die Kindprozesse ihrerseits nicht wieder neue Prozesse starten. Das angepasste Beispielprogramm sieht dann folgendermaßen aus, wobei wir die Version mit with-Anweisung zugrunde gelegt haben:

from concurrent import futures
from time import sleep, time

def test(t):
sleep(t)
print("Ich habe {} Sekunden gewartet. Zeit: {:.0f}".format(t, time()))

if __name__ == "__main__":
print("Startzeit: {:.0f}".format(time()))
with futures.ProcessPoolExecutor(max_workers=3) as e:
e.submit(test, 9)
e.submit(test, 2)
e.submit(test, 5)
e.submit(test, 6)
print("Alle Aufgaben gestartet.")
print("Alle Aufgaben erledigt.")

Neben der Verwendung von ProcessPoolExecutor anstelle von ThreadPoolExecutor haben wir mit der Abfrage der globalen Variablen __name__ geprüft, ob die Python-Datei direkt ausgeführt wird, und nur in diesem Fall die Unterprozesse gestartet. Hintergrund ist, dass die Python-Datei, in der die Unterprozesse gestartet werden, von diesen als Modul importierbar sein muss. Der Wert der globalen Variablen __name__ ist nur dann "__main__", wenn die Datei direkt mit dem Python-Interpreter ausgeführt wird. Wird die Datei hingegen als Modul importiert, hat sie einen anderen Wert, nämlich den Namen des Moduls. Durch diese Abfrage wird somit verhindert, dass ein Unterprozess beim Importieren des Moduls erneut den Code im if-Block ausführt.

[»]  Hinweis

Eine interaktive Session kann natürlich nicht von einem Unterprozess importiert werden. Daher funktioniert der ProcessPoolExecutor nicht im interaktiven Modus von Python.

Nun sind Sie in der Lage, eine Funktion parallel in verschiedenen Threads oder Prozessen auszuführen. Im nächsten Abschnitt beschäftigen wir uns damit, wie Sie auf die Rückgabewerte der Funktionsaufrufe zugreifen können.

 
Zum Seitenanfang

32.3.4    Die Verwaltung der Aufgaben eines Executors Zur vorigen ÜberschriftZur nächsten Überschrift

Die Funktion test in unserem einführenden Beispiel ist von besonders einfacher Bauart, da sie keinen Rückgabewert besitzt, den die aufrufende Ebene verwenden könnte. Interessanter sind hingegen Funktionen, die nach einer (unter Umständen langwierigen) Berechnung einen Wert zurückgeben.

Eine Modellfunktion für eine aufwendige Berechnung

Als Modellbeispiel verwenden wir im Folgenden eine Funktion, die die Kreiszahl π mithilfe des wallisschen Produkts[ 130 ](Das Produkt ist nach dem englischen Mathematiker John Wallis (1616–1703) benannt, der es im Jahre 1655 entdeckte. ) approximiert.

Im Zähler stehen dabei immer gerade Zahlen, die sich bei jedem zweiten Faktor um 2 erhöhen. Der Nenner enthält nur ungerade Zahlen, die sich mit Ausnahme des ersten Faktors ebenfalls alle zwei Faktoren um 2 erhöhen.

Die Funktion naehere_pi_an, die als Parameter die Anzahl der zu berücksichtigenden Faktoren erhält, implementieren wir folgendermaßen:

def naehere_pi_an(n):
pi_halbe = 1
zaehler, nenner = 2.0, 1.0
for i in range(n):
pi_halbe *= zaehler / nenner
if i % 2:
zaehler += 2
else:
nenner += 2
return 2*pi_halbe

Mit dem Parameter 1000 für n erzeugt die Funktion beispielsweise folgende Ausgabe, bei der nur die ersten beiden Nachkommastellen korrekt sind.

>>> naehere_pi_an(1000)
3.140023818600586

Mit größer werdendem Wert für n werden immer bessere Näherungen von π berechnet, was aber auch mit mehr Rechenzeit bezahlt werden muss. Beispielsweise benötigt ein Aufruf mit n = 30000000 auf unserem Testrechner ca. acht Sekunden.

Mit naehere_pi_an haben wir damit eine Beispielfunktion, die einen Prozessorkern voll auslastet und für größere Werte von n immer länger für die Berechnung benötigt. In den folgenden Programmen dient naehere_pi_an als Beispiel für eine aufwendige Funktion, die von einem Parameter abhängt.

Nun werden wir naehere_pi_an in mehreren Threads beziehungsweise Prozessen ausführen.

Der Zugriff auf die Ergebnisse der Berechnung

Im einführenden Beispiel haben wir die Methode submit einer Executor-Instanz verwendet, um neue Aufgaben in die Warteschlange des Executors einzureihen. Dabei erzeugt die Methode submit bei jedem Aufruf eine Instanz vom Typ futures.Future, was wir bisher ignoriert haben. Sie können sich diese Instanz wie einen Abholschein vorstellen, mit dem Sie den Status der übergebenen Aufgabe prüfen und nach Abschluss der Berechnung das Ergebnis abholen können. Im einfachsten Fall verwenden wir die Methode result der Future-Instanz, um das Ergebnis zu erhalten.

with futures.ThreadPoolExecutor(max_workers=4) as e:       
f = e.submit(naehere_pi_an, 10000000)
print(f.result())

Als Ausgabe erhalten wir wie gewünscht eine näherungsweise Berechnung von π mit 10000000 Faktoren im wallisschen Produkt. Die Methode result einer Future-Instanz blockiert die aufrufende Ebene dabei so lange, bis die zugehörige Berechnung abgeschlossen ist. Insbesondere wenn Sie gleichzeitig mehrere Aufgaben mit unterschiedlicher Laufzeit ausführen lassen, müssen Sie dadurch unter Umständen auf bereits verfügbare Ergebnisse unnötig lange warten.

with futures.ThreadPoolExecutor(max_workers=4) as e:       
f1 = e.submit(naehere_pi_an, 10000000)
f2 = e.submit(naehere_pi_an, 100)
print("f1:", f1.result())
print("f2:", f2.result())

In diesem Beispiel werden zwei Berechnungen durchgeführt, wobei die eine deutlich länger rechnet als die andere. Trotzdem wird zunächst das Ergebnis der Rechnung mit n=10000000 und erst danach das zu n=100 ausgegeben.

f1: 3.1415924965090136
f2: 3.126078900215409

Das Modul concurrent.futures stellt Funktionen bereit, um bequem auf die Ergebnisse mehrerer Berechnungen zuzugreifen.

concurrent.futures.as_completed(fs, [timeout])

Mit der Funktion as_completed wird ein Iterator erzeugt, der über die Ergebnisse des Containers fs von Future-Instanzen iteriert. Dabei werden die Ergebnisse in der Reihenfolge durchlaufen, in der sie zur Verfügung stehen. Dadurch kann die aufrufende Ebene die Ergebnisse sofort weiterverarbeiten, wenn die jeweilige Berechnung abgeschlossen ist.

N = (12345678,  123456,  1234,  12)
with futures.ThreadPoolExecutor(max_workers=4) as e:
fs = {e.submit(naehere_pi_an, n): n for n in N}
for f in futures.as_completed(fs):
print("n={:10}: {}".format(fs[f], f.result()))

Im Beispiel lassen wir die Funktion naehere_pi_an mit verschiedenen Parametern durch einen ThreadPoolExecutor mit 4 Threads ausführen. Durch die Verwendung von futures.as_completed erhalten wir die folgende Ausgabe, bei der die Ergebnisse der kürzeren Rechnungen zuerst ausgegeben werden:

n=        12: 3.02317019200136
n= 1234: 3.1403210113038207
n= 123456: 3.1415799301866607
n= 12345678: 3.1415925263536626

Das Dictionary fs verwenden wir, um uns die Zuordnung von Eingabeparametern zur zugehörigen Future-Instanz zu merken. Interessant ist noch, dass wir wie oben die Methode result der Future-Instanzen verwenden, um das Ergebnis der Berechnung abzufragen. Da die Funktion futures.as_completed nur die Future-Instanzen liefert, deren Berechnung schon abgeschlossen ist, muss result nicht auf die Vollendung der Berechnung warten, sondern liefert sofort den bereits bestimmten Wert.

Mit dem Parameter timeout kann optional eine Zeit in Sekunden angegeben werden, die futures.as_completed auf die Berechnung aller Ergebnisse warten soll. Ist nach timeout Sekunden das letzte Ergebnis noch nicht berechnet, wird eine TimeoutError-Exception geworfen.

concurrent.futures.wait(fs, [timeout, return_when])

Die Funktion futures.wait wartet so lange, bis das von return_when spezifizierte Ereignis eingetreten oder die Zeit timeout abgelaufen ist. Der Rückgabewert ist eine Instanz, die die beiden Attribute done und not_done besitzt. Dabei referenziert done die Menge der Future-Instanzen in fs, die zum Zeitpunkt des Ereignisses bereits abgearbeitet worden sind, während not_done die noch ausstehenden Future-Instanzen in einer Menge enthält.

Die möglichen Werte für return_when sind in Tabelle 32.3 aufgelistet.

Konstante Bedeutung
futures.FIRST_COMPLETED Die erste Aufgabe in fs ist fertiggestellt.
futures.FIRST_EXCEPTION Die erste Aufgabe in fs wirft eine Exception.
Wird keine Exception geworfen, wird gewartet, bis alle Aufgaben in fs abgearbeitet sind.
futures.ALL_COMPLETED Alle Aufgaben in fs sind fertiggestellt.

Tabelle 32.3    Die von futures.wait unterstützten Ereignisse

Standardmäßig wird gewartet, bis alle Aufgaben abgearbeitet sind, wie im folgenden Beispiel gezeigt.

with futures.ThreadPoolExecutor(max_workers=4) as e:
fs = {e.submit(naehere_pi_an, n): n for n in N}
res = futures.wait(fs)
for f in res.done:
print("n={:10}: {}".format(fs[f], f.result()))

Ähnlich wie bei futures.as_completed kann mit timeout eine Maximalzeit in Sekunden angegeben werden, die wait abwartet, bevor es die Kontrolle wieder an die aufrufende Ebene zurückgibt. Das folgende Beispiel gibt jede Sekunde die fertigen Ergebnisse aus, bis keine mehr übrig sind.

with futures.ThreadPoolExecutor(max_workers=4) as e:
fs = {e.submit(naehere_pi_an, n): n for n in N}
fertig = False
while not fertig:
res = futures.wait(fs, timeout=1.0)
for f in res.done:
print("n={:10}: {}".format(fs[f], f.result()))
del fs[f]
fertig = (len(res.not_done) == 0)

Durch die Festlegung eines Timeouts kann sichergestellt werden, dass die aufrufende Ebene in regelmäßigen Abständen die Kontrolle zurückerhält, während auf das nächste Ergebnis gewartet wird.

[»]  Hinweis

Sowohl bei der Funktion futures.as_completed als auch bei futures.wait kann fs auch Futures-Instanzen enthalten, die zu verschiedenen Executor-Instanzen gehören.

Executor.map(func, [*iterables], {timeout, chunksize})

Die Executor-Instanzen selbst besitzen eine Methode map, mit der sich eine Funktion auf alle Werte in iterierbaren Objekten, also beispielsweise Listen, anwenden lässt. Sie verhält sich also genauso wie die Built-in Function map[ 131 ](Die Beschreibung zur Built-in Function map finden Sie in Abschnitt 19.8.30. ), mit der Ausnahme, dass sie die Funktion func mithilfe des Executors parallel in Threads oder Prozessen ausführen kann.

Mit dem Parameter timeout kann eine Zeitspanne in Sekunden festgelegt werden, die das Abarbeiten der Funktionsaufrufe insgesamt dauern darf. Dauert die Berechnung länger, wird eine concurrent.futures.TimeoutError-Exception geworfen. Standardmäßig ist die erlaubte Zeit unbegrenzt.

Mit chunksize kann dafür gesorgt werden, dass die Elemente von iterables blockweise abgearbeitet werden. Wird beispielsweise eine Liste mit 100 Elementen als iterables zusammen mit chunksize=20 übergeben, werden 5 Pakete mit jeweils 20 Elementen verarbeitet. Standardmäßig wird für jedes Element ein neuer Prozess gestartet. Ein von 1 abweichender Wert von chunksize kann die Verarbeitungsgeschwindigkeit gegebenenfalls stark verbessern. Beachten Sie, dass chunksize nur bei Verwendung der Klasse ProcessPoolExecutor von Bedeutung ist.

Nun werden wir untersuchen, wie sich die Verwendung von Threads und Prozessen auf die Laufzeit eines Programms auswirkt.

Rechnungen auf mehreren Prozessoren mit ProcessPoolExecutor

Das folgende Beispiel ruft die Funktion naehere_pi_an für eine Liste relativ großer Werte für n auf, wobei mithilfe eines Kommandozeilenparameters festgelegt wird, ob die Built-in Function map oder Executor.map mit Threads beziehungsweise Prozessen verwendet werden soll. Das Programm errechnet die Laufzeit, indem es die Differenz zwischen Start- und Endzeit berechnet und ausgibt.

from concurrent import futures
import sys
import time

def naehere_pi_an(n):
...

if __name__ == "__main__":
start = time.perf_counter()
N = (34567890, 5432198, 44444444, 22222222, 56565656,
43236653, 23545353, 32425262)
if sys.argv[1] == "threads":
with futures.ThreadPoolExecutor(max_workers=4) as e:
res = e.map(naehere_pi_an, N)
elif sys.argv[1] == "processes":
with futures.ProcessPoolExecutor(max_workers=4) as e:
res = e.map(naehere_pi_an, N)
else:
res = map(naehere_pi_an, N)
print(list(res))
print(time.perf_counter()-start)

Auf einem Beispielrechner mit vier Prozessorkernen haben sich folgende Ausgaben ergeben, wobei die Liste der Ergebnisse weggelassen wurde.

$ python benchmark.py builtin
70.19322323799133
$ python benchmark.py threads
89.58459234237671
$ python benchmark.py processes
26.78869891166687

Wie Sie sehen, ist der Durchlauf mit vier Worker-Prozessen um etwa den Faktor 2,5 schneller als der Durchlauf mit der Built-In-Funktion map. Dies liegt daran, dass das Programm alle vier Kerne des Rechners nutzen konnte, um die Berechnung zu beschleunigen. Die Berechnung lief also tatsächlich parallel auf verschiedenen Prozessorkernen ab.

Auf den ersten Blick verwunderlich ist die im Vergleich zur Built-in Function map verlängerte Laufzeit beim Durchlauf mit vier Worker-Threads. Wenn man aber bedenkt, dass aufgrund der Einschränkungen von CPython immer nur ein Thread zur gleichen Zeit ausgeführt werden kann, wird klar, dass man keinen Geschwindigkeitszuwachs erwarten kann. Dass die Threads-Variante sogar langsamer ist, liegt an dem zusätzlichen Aufwand, der für die Verwaltung der Threads betrieben werden muss. In CPython eignen sich Threads also nicht für die Beschleunigung aufwendiger Rechnungen, sondern nur, um ein Blockieren des Programms zu verhindern.

[»]  Hinweis

Die Funktion perf_counter des Moduls time ist ein Zeitgeber speziell zum Messen von Programmlaufzeiten. Nähere Informationen finden Sie im Abschnitt 17.1.2.

Der Umgang mit Instanzen des Typs futures.Future

Bisher haben wir Instanzen des Typs futures.Future nur dazu genutzt, um die Ergebnisse von Executor-Aufgaben auszulesen. Tatsächlich bieten sie eine Reihe weiterer Methoden, um ihren aktuellen Status zu erfragen oder ihre Ausführung zu beeinflussen.

Tabelle 32.4 listet diese Methoden auf.

Methode Beschreibung
cancel() Versucht, die Aufgabe abzubrechen, und gibt bei Erfolg True, ansonsten False zurück.
cancelled() Gibt True zurück, wenn die Aufgabe abgebrochen wurde, sonst False.
running() Gibt True zurück, wenn die Aufgabe gerade ausgeführt wird, sonst False.
done() Ist der Rückgabewert True, wurde die Aufgabe abgeschlossen oder abgebrochen.
result([timeout]) Liefert das Ergebnis der Aufgabe, wobei maximal timeout Sekunden gewartet wird.
exception([timeout]) Liefert die in der Aufgabe-Funktion geworfene Exception, wobei maximal timeout Sekunden gewartet wird. Falls die Aufgabe erfolgreich abgeschlossen wird, wird None zurückgegeben.
add_done_callback(fn) Sorgt dafür, dass die Funktion fn mit dem Ergebnis der Aufgabe aufgerufen wird, sobald das Ergebnis vorliegt. Die Methode add_done_callback kann mehrfach für verschiedene Funktionen aufgerufen werden.

Tabelle 32.4    Die Methoden einer Instanz des Typs futures.Future

 


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