2.1 Modul threading
Multithreading in Python bedeutet, dass mehrere Threads (threads)
gleichzeitig ausgeführt werden können. Das ermöglicht eine effizientere Nutzung der Prozessor-Ressourcen, besonders bei I/O-Operationen oder anderen Aufgaben, die parallel ausgeführt werden können.
Grundlegende Begriffe des Multithreadings in Python:
Thread – die kleinste Ausführungseinheit, die parallel mit anderen Threads innerhalb eines Prozesses arbeiten kann. Alle Threads in einem Prozess teilen sich denselben Speicher, was den Datenaustausch zwischen den Threads ermöglicht.
Prozess – ein laufendes Programm in einem Betriebssystem mit einem eigenen Adressraum und eigenen Ressourcen. Im Gegensatz zu Threads sind Prozesse voneinander isoliert und tauschen Daten über Interprozesskommunikation (IPC)
aus.
GIL
– ein Mechanismus im Python-Interpreter, der verhindert, dass mehrere Python-Threads gleichzeitig ausgeführt werden. Das GIL
stellt die Sicherheit bei der Ausführung von Python-Code sicher, begrenzt jedoch die Performance von multithreaded Programmen auf Mehrkern-Prozessoren.
Wichtig! Aufgrund des Global Interpreter Locks (GIL) sorgt Multithreading in Python möglicherweise nicht für eine deutliche Leistungssteigerung bei rechenintensiven Aufgaben, da das GIL die gleichzeitige Ausführung mehrerer Python-Threads auf Mehrkern-Prozessoren verhindert.
Das Modul threading
Das Modul threading
in Python bietet eine High-Level-Schnittstelle für die Arbeit mit Threads. Es erlaubt das Erstellen und Verwalten von Threads, deren Synchronisation sowie die Organisation der Interaktion zwischen ihnen. Lass uns die wichtigsten Komponenten und Funktionen dieses Moduls näher anschauen.
Wichtige Komponenten des Moduls threading
Entitäten für die Arbeit mit Threads:
-
Thread
– die Hauptklasse zum Erstellen und Verwalten von Threads. -
Timer
– ein Timer, der eine Funktion nach einer bestimmten Zeit ausführt. -
ThreadLocal
– ermöglicht das Erstellen von threadlokalen Daten.
Mechanismen zur Synchronisation von Threads:
-
Lock
– ein primitiver Synchronisationsmechanismus, um gleichzeitigen Zugriff auf gemeinsame Ressourcen zu verhindern. -
Condition
– eine Bedingungsvariable für komplexere Thread-Synchronisationen. Event
– ein Mechanismus für die Benachrichtigung zwischen Threads.-
Semaphore
– ein Mechanismus zur Begrenzung der Anzahl der Threads, die gleichzeitig einen bestimmten Abschnitt ausführen können. -
Barrier
– synchronisiert eine bestimmte Anzahl von Threads, indem sie blockiert werden, bis alle den Barrierepunkt erreicht haben.
Im Folgenden erkläre ich dir drei Klassen zur Arbeit mit Threads; die Mechanismen zur Synchronisation von Threads wirst du in naher Zukunft wahrscheinlich nicht brauchen.
2.2 Klasse Thread
Die Klasse Thread
ist die Hauptklasse zum Erstellen und Verwalten von Threads. Sie bietet vier Hauptmethoden:
start()
: Startet die Ausführung des Threads.-
join()
: Der aktuelle Thread wird angehalten und wartet auf die Beendigung des gestarteten Threads. is_alive()
: GibtTrue
zurück, wenn der Thread aktiv ist.-
run()
: Eine Methode, die den Code enthält, der im Thread ausgeführt wird. Diese Methode wird beim Erben von der KlasseThread
überschrieben.
Alles ist tatsächlich einfacher, als es scheint – hier ein Beispiel für die Verwendung der Klasse Thread
.
Start eines einfachen Threads
import threading
def worker():
print("Worker thread is running")
# Erstellen eines neuen Threads
t = threading.Thread(target=worker) # Neuer Thread-Objekt erstellt
t.start() # Thread gestartet
t.join() # Warten auf das Ende des Threads
print("Main thread is finished")
Nach dem Aufruf der Methode start beginnt die Funktion worker mit ihrer Ausführung. Oder genauer gesagt: Ihr Thread wird der Liste der aktiven Threads hinzugefügt.
Verwendung von Argumenten
import threading
def worker(number, text):
print(f"Worker {number}: {text}")
# Neuer Thread mit Argumenten
t = threading.Thread(target=worker, args=(1, "Hello"))
t.start()
t.join()
Um Parameter an einen neuen Thread zu übergeben, musst du sie lediglich als Tuple angeben und dem Parameter args
zuweisen. Beim Aufruf der Ziel-Funktion, die im target
angegeben wurde, werden die Parameter automatisch übergeben.
Überschreiben der Methode run
import threading
class MyThread(threading.Thread):
def run(self):
print("Custom thread is running")
# Erstellen und Starten eines Threads
t = MyThread()
t.start()
t.join()
Es gibt zwei Möglichkeiten, eine Funktion anzugeben, mit der die Ausführung eines neuen Threads beginnen soll: Entweder über den Parameter target
beim Erstellen des Thread
-Objekts oder durch Erben von der Klasse Thread
und Überschreiben der Funktion run
. Beide Ansätze sind legitim und werden häufig verwendet.
2.3 Klasse Timer
Die Klasse Timer
im Modul threading
dient dazu, eine Funktion nach einem bestimmten Zeitintervall aufzurufen. Diese Klasse ist nützlich, um verzögerte Aufgaben in einer Multithread-Umgebung auszuführen.
Der Timer wird mit der Funktion, die aufgerufen werden soll, und dem Verzögerungszeitpunkt in Sekunden initialisiert.
-
Methode
start()
startet den Timer, der das angegebene Zeitintervall zählt und dann die angegebene Funktion ausführt. -
Methode
cancel()
ermöglicht das Stoppen des Timers, falls er noch nicht ausgelöst wurde. Das ist nützlich, um die Funktion nicht auszuführen, wenn der Timer nicht mehr benötigt wird.
Anwendungsbeispiele:
Funktion mit Verzögerung aufrufen
In diesem Beispiel wird die Funktion hello
5 Sekunden nach dem Start des Timers aufgerufen.
import threading
def hello():
print("Hello, world!")
# Erstellen eines Timers, der die Funktion hello nach 5 Sekunden aufruft
t = threading.Timer(5.0, hello)
t.start() # Timer starten
Timer vor der Ausführung stoppen
In diesem Fall wird der Timer gestoppt, bevor die Funktion hello
ausgeführt werden kann; entsprechend wird nichts ausgegeben.
import threading
def hello():
print("Hello, world!")
# Erstellen eines Timers
t = threading.Timer(5.0, hello)
t.start() # Timer starten
# Timer vor der Ausführung stoppen
t.cancel()
Timer mit Argumenten
In diesem Beispiel ruft der Timer die Funktion greet
nach 3 Sekunden auf und übergibt ihr das Argument "Alice"
.
import threading
def greet(name):
print(f"Hello, {name}!")
# Erstellen eines Timers mit Argumenten
t = threading.Timer(3.0, greet, args=["Alice"])
t.start()
Die Klasse Timer
ist praktisch für die Planung von Aufgaben, die nach einer bestimmten Zeit ausgeführt werden sollen. Allerdings garantieren Timer keine absolut präzise Ausführungszeit, da dies von der Systemlast und dem Thread-Scheduler abhängt.
2.4 Klasse ThreadLocal
Die Klasse ThreadLocal
dient dazu, Threads mit eigenen lokalen Daten zu erstellen. Dies ist in Multithreaded-Anwendungen nützlich, wenn jeder Thread seine eigene Version von Daten haben muss, um Konflikte und Synchronisationsprobleme zu vermeiden.
Jeder Thread, der ThreadLocal
verwendet, hat seine eigenen unabhängigen Kopien der Daten. Daten, die in einem ThreadLocal
-Objekt gespeichert sind, sind für jeden Thread einzigartig und werden nicht mit anderen Threads geteilt. Das ist praktisch für die Speicherung von Daten, die nur im Kontext eines Threads verwendet werden, wie z. B. aktuelle Benutzer in Webanwendungen oder die aktuelle Verbindung zur Datenbank.
Anwendungsbeispiele:
Grundlegende Verwendung
In diesem Beispiel weist jeder Thread seinen Namen einer lokalen Variablen value
zu und gibt diesen aus. Der Wert value
ist für jeden Thread einzigartig.
import threading
# Erstellen eines ThreadLocal-Objekts
local_data = threading.local()
def process_data():
# Wert der lokalen Thread-Variablen zuweisen
local_data.value = threading.current_thread().name
# Zugriff auf die lokale Thread-Variable
print(f'Value in {threading.current_thread().name}: {local_data.value}')
threads = []
for i in range(5):
t = threading.Thread(target=process_data)
threads.append(t)
t.start()
for t in threads:
t.join()
Speicherung von Benutzerdaten in einer Webanwendung
In diesem Beispiel verarbeitet jeder Thread eine Anfrage für seinen Benutzer. Der Wert user_data.user
ist für jeden Thread einzigartig.
import threading
# Erstellen eines ThreadLocal-Objekts
user_data = threading.local()
def process_request(user):
# Wert der lokalen Thread-Variablen zuweisen
user_data.user = user
handle_request()
def handle_request():
# Zugriff auf die lokale Thread-Variable
print(f'Handling request for user: {user_data.user}')
threads = []
users = ['Alice', 'Bob', 'Charlie']
for user in users:
t = threading.Thread(target=process_request, args=(user,))
threads.append(t)
t.start()
for t in threads:
t.join()
Das waren die drei nützlichsten Klassen der Bibliothek threading
. Wahrscheinlich wirst du diese in deiner Arbeit verwenden, während die anderen Klassen weniger relevant sind. Heutzutage nutzen viele Leute lieber asynchrone Funktionen und die Bibliothek asyncio
. Und genau darüber sprechen wir demnächst.
GO TO FULL VERSION