In diesem Tutorial erkunden wir die Nutzung des integrierten Threading-Moduls von Python, um die Möglichkeiten des Multithreadings in Python zu verstehen und anzuwenden.
Wir beginnen mit einer Einführung in die grundlegenden Konzepte von Prozessen und Threads, um die Funktionsweise von Multithreading in Python zu verdeutlichen und gleichzeitig die Ideen von Nebenläufigkeit und Parallelität zu beleuchten. Anschließend erfahren Sie, wie Sie mit Hilfe des integrierten Threading-Moduls einen oder mehrere Threads in Python initialisieren und ausführen.
Legen wir los.
Prozesse versus Threads: Die wesentlichen Unterschiede
Was genau ist ein Prozess?
Ein Prozess stellt jede laufende Instanz eines Programms dar.
Dies kann alles sein – von einem einfachen Python-Skript über einen Webbrowser wie Chrome bis hin zu einer komplexen Videokonferenz-Anwendung. Um die aktiven Prozesse und Threads auf Ihrem Computer zu inspizieren, können Sie den Task-Manager öffnen und unter der Rubrik „Leistung -> CPU“ die derzeit auf Ihren CPU-Kernen laufenden Prozesse und Threads einsehen.
Ein tieferes Verständnis von Prozessen und Threads
Intern verfügt jeder Prozess über einen eigenen, dedizierten Speicherbereich, in dem der zugehörige Code und die Daten abgelegt werden.
Ein Prozess setzt sich aus einem oder mehreren Threads zusammen. Ein Thread ist die kleinste ausführbare Einheit von Anweisungen, die vom Betriebssystem verarbeitet werden kann, und repräsentiert den Ablauf der Ausführung.
Jeder Thread hat seinen eigenen Stack und Register, aber keinen eigenen Speicherbereich. Alle Threads innerhalb eines Prozesses haben Zugriff auf die Daten des Prozesses. Das bedeutet, dass Daten und Speicher von allen Threads eines Prozesses gemeinsam genutzt werden.
Auf einer CPU mit N Kernen können N Prozesse gleichzeitig parallel ausgeführt werden. Zwei Threads desselben Prozesses können jedoch nicht wirklich parallel laufen, sondern werden gleichzeitig ausgeführt. Im nächsten Abschnitt gehen wir näher auf das Konzept von Nebenläufigkeit im Vergleich zur Parallelität ein.
Fassen wir basierend auf dem bisherigen Wissen die Unterschiede zwischen Prozessen und Threads zusammen.
Merkmal | Prozess | Thread |
Speicher | Dedizierter Speicher | Gemeinsam genutzter Speicher |
Ausführungsmodus | Parallel, nebenläufig | Nebenläufig; aber nicht parallel |
Ausführung verwaltet durch | Betriebssystem | CPython Interpreter |
Multithreading in Python
In Python stellt der Global Interpreter Lock (GIL) sicher, dass zu einem bestimmten Zeitpunkt immer nur ein Thread die Ausführungslizenz (Lock) erhält und damit arbeiten kann. Jeder Thread muss diese Sperre erwerben, um ausgeführt zu werden. Dadurch wird sichergestellt, dass immer nur ein einziger Thread zu einem gegebenen Zeitpunkt aktiv sein kann, was echtes gleichzeitiges Multithreading verhindert.
Stellen Sie sich beispielsweise zwei Threads, t1 und t2, vor, die demselben Prozess angehören. Da diese Threads denselben Datenspeicher nutzen, könnte t2 einen Wert k verändern, nachdem t1 ihn gelesen hat. Dies kann zu Deadlocks und unerwünschten Ergebnissen führen. Die GIL garantiert jedoch, dass immer nur ein Thread die Sperre besitzt und damit arbeitet. Hieraus resultiert die Thread-Sicherheit.
Wie erreichen wir also die Multithreading-Funktion in Python? Um dies zu verstehen, müssen wir die Konzepte von Nebenläufigkeit und Parallelität genauer betrachten.
Nebenläufigkeit vs. Parallelität: Eine Übersicht
Nehmen wir an, wir arbeiten mit einer CPU, die über mehrere Kerne verfügt. In der untenstehenden Abbildung sehen wir eine CPU mit vier Kernen. Dies bedeutet, dass wir zu jedem Zeitpunkt vier verschiedene Operationen parallel ausführen können.
Wenn wir vier Prozesse haben, kann jeder von ihnen unabhängig und parallel auf einem der vier Kerne laufen. Nehmen wir an, jeder dieser Prozesse hat zwei Threads.
Um das Threading besser zu verstehen, wechseln wir nun von einer Multicore-Architektur zu einer Singlecore-Prozessorarchitektur. Wie bereits erwähnt, kann zu jedem Zeitpunkt nur ein einziger Thread aktiv sein. Der Prozessorkern kann jedoch zwischen verschiedenen Threads wechseln.
E/A-gebundene Threads, die beispielsweise auf E/A-Operationen warten – wie das Einlesen von Benutzereingaben, Datenbanklesevorgänge und Dateioperationen – können während der Wartezeit die Sperre freigeben, damit ein anderer Thread ausgeführt werden kann. Die Wartezeit kann aber auch ein einfacher Vorgang sein, wie zum Beispiel das Schlafen für n Sekunden.
Zusammengefasst: Während Warteoperationen gibt der Thread die Sperre frei und ermöglicht es dem Prozessorkern, zu einem anderen Thread zu wechseln. Der vorherige Thread setzt seine Ausführung fort, sobald die Wartezeit abgelaufen ist. Dieser Prozess des Umschaltens zwischen Threads durch den Prozessorkern ermöglicht das Multithreading. ✅
Wenn Sie Parallelität auf Prozessebene in Ihrer Anwendung realisieren möchten, sollten Sie stattdessen die Verwendung von Multiprocessing in Betracht ziehen.
Das Python Threading Modul: Erste Schritte
Python verfügt über ein eigenes Modul zum Threading, das Sie in Ihre Python-Skripte importieren können.
import threading
Um ein Thread-Objekt in Python zu erstellen, nutzen Sie den Thread-Konstruktor: threading.Thread(...)
. Die allgemeine Syntax sieht wie folgt aus und ist für die meisten Threading-Implementierungen ausreichend:
threading.Thread(target=..., args=...)
Hierbei gilt:
target
ist ein Schlüsselwortargument, das eine aufrufbare Python-Funktion referenziert.args
ist ein Tupel von Argumenten, die die Zielfunktion benötigt.
Für die Code-Beispiele in diesem Tutorial benötigen Sie Python 3.x. Laden Sie den Code herunter und folgen Sie den Anweisungen.
Threads in Python definieren und ausführen
Erstellen wir einen Thread, der eine Zielfunktion ausführt.
Die Zielfunktion ist in diesem Fall some_func
.
import threading
import time
def some_func():
print("Running some_func...")
time.sleep(2)
print("Finished running some_func.")
thread1 = threading.Thread(target=some_func)
thread1.start()
print(threading.active_count())
Lassen Sie uns die Funktion des oben gezeigten Code-Snippets analysieren:
- Es werden die Module
threading
undtime
importiert. - Die Funktion
some_func
beinhaltet beschreibendeprint()
-Anweisungen sowie eine Pause von zwei Sekunden durchtime.sleep(n)
. - Anschließend erstellen wir einen Thread
thread_1
mit dem Zielsome_func
.threading.Thread(target=...)
generiert ein Thread-Objekt. - Wichtig: Verwenden Sie den Funktionsnamen und keinen Funktionsaufruf; also
some_func
und nichtsome_func()
. - Das Erstellen eines Thread-Objekts startet noch keinen Thread. Erst durch den Aufruf der
start()
-Methode des Thread-Objekts wird dieser gestartet. - Um die Anzahl der aktiven Threads zu erhalten, verwenden wir die Funktion
active_count()
.
Das Python-Skript läuft im Haupt-Thread, und wir generieren einen zusätzlichen Thread (thread1
), um die Funktion some_func
auszuführen. Dadurch ergibt sich eine Anzahl von zwei aktiven Threads, wie in der Ausgabe sichtbar:
# Output
Running some_func...
2
Finished running some_func.
Bei genauerer Betrachtung der Ausgabe stellen wir fest, dass beim Start von thread1
die erste print
-Anweisung ausgeführt wird. Während des Schlafvorgangs wechselt der Prozessor jedoch zum Haupt-Thread und gibt die Anzahl der aktiven Threads aus – ohne darauf zu warten, dass thread1
seine Ausführung beendet.
Warten auf das Ende der Thread-Ausführung
Falls Sie sicherstellen möchten, dass thread1
seine Ausführung beendet, können Sie nach dem Start des Threads die join()
-Methode für diesen Thread aufrufen. Dadurch wird gewartet, bis thread1
die Ausführung beendet hat, ohne zum Haupt-Thread zu wechseln.
import threading
import time
def some_func():
print("Running some_func...")
time.sleep(2)
print("Finished running some_func.")
thread1 = threading.Thread(target=some_func)
thread1.start()
thread1.join()
print(threading.active_count())
Nun hat thread1
seine Ausführung abgeschlossen, bevor wir die Anzahl der aktiven Threads ausgeben. Es läuft also nur noch der Haupt-Thread, was bedeutet, dass die Anzahl der aktiven Threads eins beträgt. ✅
# Output
Running some_func...
Finished running some_func.
1
Ausführen mehrerer Threads in Python
Als Nächstes erstellen wir zwei Threads, um zwei verschiedene Funktionen auszuführen.
Hier ist count_down
eine Funktion, die eine Zahl als Argument entgegennimmt und von dieser Zahl bis auf Null herunterzählt.
def count_down(n):
for i in range(n,-1,-1):
print(i)
Wir definieren count_up
, eine weitere Python-Funktion, die von Null bis zu einer bestimmten Zahl hochzählt.
def count_up(n):
for i in range(n+1):
print(i)
📑 Bei Verwendung der Funktion range()
mit der Syntax range(start, stop, step)
wird der Endpunkt stop
standardmäßig ausgeschlossen.
– Um von einer gegebenen Zahl bis auf Null herunterzuzählen, können Sie einen negativen Schrittwert von -1 verwenden und den stop
-Wert auf -1 setzen, sodass Null mit eingeschlossen wird.
– Ebenso müssen Sie, um bis n hochzuzählen, den stop
-Wert auf n + 1
setzen. Da die Standardwerte von start
und step
0 bzw. 1 sind, können Sie range(n + 1)
verwenden, um die Sequenz 0 bis n zu erhalten.
Als Nächstes definieren wir zwei Threads, thread1
und thread2
, um die Funktionen count_down
bzw. count_up
auszuführen. Wir ergänzen print
-Anweisungen und Schlafoperationen für beide Funktionen.
Beachten Sie beim Erstellen der Thread-Objekte, dass die Argumente für die Zielfunktion als Tupel angegeben werden müssen – für den Parameter args
. Da beide Funktionen (count_down
und count_up
) ein Argument annehmen, müssen Sie explizit ein Komma nach dem Wert einfügen. Dadurch wird sichergestellt, dass das Argument weiterhin als Tupel übergeben wird, da die nachfolgenden Elemente sonst als None
interpretiert werden.
import threading
import time
def count_down(n):
for i in range(n,-1,-1):
print("Running thread1....")
print(i)
time.sleep(1)
def count_up(n):
for i in range(n+1):
print("Running thread2...")
print(i)
time.sleep(1)
thread1 = threading.Thread(target=count_down,args=(10,))
thread2 = threading.Thread(target=count_up,args=(5,))
thread1.start()
thread2.start()
In der Ausgabe:
- Die Funktion
count_up
läuft aufthread2
und zählt von 0 bis 5 hoch. - Die Funktion
count_down
läuft aufthread1
und zählt von 10 auf 0 herunter.
# Output
Running thread1....
10
Running thread2...
0
Running thread1....
9
Running thread2...
1
Running thread1....
8
Running thread2...
2
Running thread1....
7
Running thread2...
3
Running thread1....
6
Running thread2...
4
Running thread1....
5
Running thread2...
5
Running thread1....
4
Running thread1....
3
Running thread1....
2
Running thread1....
1
Running thread1....
0
Sie können beobachten, dass thread1
und thread2
abwechselnd ausgeführt werden, da beide eine Warteoperation (sleep
) beinhalten. Sobald die Funktion count_up
das Zählen bis 5 beendet hat, ist thread2
nicht mehr aktiv. Daher sehen wir anschließend nur noch die Ausgaben, die zu thread1
gehören.
Zusammenfassung
In diesem Tutorial haben Sie erfahren, wie Sie das integrierte Threading-Modul von Python verwenden, um Multithreading zu realisieren. Hier ist eine Zusammenfassung der wichtigsten Punkte:
- Der Thread-Konstruktor kann verwendet werden, um ein Thread-Objekt zu erstellen. Mit
threading.Thread(target=<callable>, args=(<tuple of args>))
wird ein Thread erstellt, der das aufrufbare Ziel mit den inargs
definierten Argumenten ausführt. - Das Python-Programm läuft auf einem Haupt-Thread, sodass die von Ihnen generierten Thread-Objekte zusätzliche Threads darstellen. Sie können die Funktion
active_count()
verwenden, um die Anzahl der aktiven Threads zu jedem Zeitpunkt abzurufen. - Sie können einen Thread mit der
start()
-Methode des Thread-Objekts starten und mit derjoin()
-Methode auf das Ende der Ausführung warten.
Sie können weitere Experimente mit den Code-Beispielen durchführen, indem Sie die Wartezeiten anpassen, verschiedene E/A-Operationen ausprobieren und vieles mehr. Zögern Sie nicht, Multithreading in Ihre kommenden Python-Projekte einzubauen. Viel Spaß beim Programmieren! 🎉