Python-Threading: Eine Einführung – wdzwdz

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 und time importiert.
  • Die Funktion some_func beinhaltet beschreibende print()-Anweisungen sowie eine Pause von zwei Sekunden durch time.sleep(n).
  • Anschließend erstellen wir einen Thread thread_1 mit dem Ziel some_func. threading.Thread(target=...) generiert ein Thread-Objekt.
  • Wichtig: Verwenden Sie den Funktionsnamen und keinen Funktionsaufruf; also some_func und nicht some_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 auf thread2 und zählt von 0 bis 5 hoch.
  • Die Funktion count_down läuft auf thread1 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 in args 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 der join()-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! 🎉