Das `threading`-Modul in Python

Das `threading`-Modul in Python

Dieser Artikel erklärt das threading-Modul in Python.

YouTube Video

Das threading-Modul in Python

Das threading-Modul in Python ist eine Standardbibliothek, die die Mehrfadenprogrammierung unterstützt. Durch die Verwendung von Threads können mehrere Prozesse gleichzeitig ausgeführt werden, was die Leistung von Programmen verbessern kann, insbesondere bei blockierenden Operationen wie I/O-Wartezeiten. Aufgrund von Pythons Global Interpreter Lock (GIL) ist die Effektivität von Multithreading bei CPU-intensiven Operationen begrenzt, aber es funktioniert effizient für I/O-intensiven Operationen.

Die folgenden Abschnitte erklären die Grundlagen der Verwendung des threading-Moduls und die Steuerung von Threads.

Grundlegende Nutzung von Threads

Erstellen und Ausführen von Threads

Um einen Thread zu erstellen und parallele Verarbeitung durchzuführen, verwenden Sie die Klasse threading.Thread. Geben Sie eine Ziel-Funktion an, um einen Thread zu erstellen und diesen auszuführen.

 1import threading
 2import time
 3
 4# Function to be executed in a thread
 5def worker():
 6    print("Worker thread started")
 7    time.sleep(2)
 8    print("Worker thread finished")
 9
10# Create and start the thread
11thread = threading.Thread(target=worker)
12thread.start()
13
14# Processing in the main thread
15print("Main thread continues to run")
16
17# Wait for the thread to finish
18thread.join()
19print("Main thread finished")
  • In diesem Beispiel wird die Funktion worker in einem separaten Thread ausgeführt, während der Haupt-Thread weiterhin arbeitet. Durch Aufrufen der Methode join() wartet der Haupt-Thread auf die Fertigstellung des Neben-Threads.

Benennung von Threads

Threads aussagekräftige Namen zu geben, erleichtert Logging und Debugging. Sie können dies mit dem Argument name festlegen.

 1import threading
 2import time
 3
 4# Function to be executed in a thread
 5def worker():
 6    print("Worker thread started")
 7    time.sleep(2)
 8    print("Worker thread finished")
 9
10t = threading.Thread(
11    target=worker,
12    args=("named-worker", 0.3),
13    name="MyWorkerThread"
14)
15
16t.start()
17
18print("Active threads:", threading.active_count())
19for th in threading.enumerate():
20    print(" -", th.name)
21
22t.join()
  • threading.enumerate() gibt eine Liste der aktuellen Threads zurück, was beim Debuggen und Überwachen des Status nützlich ist.

Vererbung der Thread-Klasse

Wenn Sie die Thread-ausführende Klasse anpassen möchten, können Sie eine neue Klasse definieren, indem Sie die Klasse threading.Thread erben.

 1import threading
 2import time
 3
 4# Inherit from the Thread class
 5class WorkerThread(threading.Thread):
 6    def __init__(self, name, delay, repeat=3):
 7        super().__init__(name=name)
 8        self.delay = delay
 9        self.repeat = repeat
10        self.results = []
11
12    def run(self):
13        for i in range(self.repeat):
14            msg = f"{self.name} step {i+1}"
15            print(msg)
16            self.results.append(msg)
17            time.sleep(self.delay)
18
19# Create and start the threads
20t1 = WorkerThread("Worker-A", delay=0.4, repeat=3)
21t2 = WorkerThread("Worker-B", delay=0.2, repeat=5)
22
23t1.start()
24t2.start()
25
26t1.join()
27t2.join()
28
29print("Results A:", t1.results)
30print("Results B:", t2.results)
  • In diesem Beispiel wird die Methode run() überschrieben, um das Verhalten des Threads zu definieren, wodurch jeder Thread eigene Daten verwalten kann. Dies ist nützlich, wenn Threads komplexe Verarbeitungen durchführen oder wenn jeder Thread über eigene, unabhängige Daten verfügen soll.

Synchronisation zwischen Threads

Wenn mehrere Threads gleichzeitig auf gemeinsame Ressourcen zugreifen, können Datenrennen auftreten. Um dies zu verhindern, bietet das threading-Modul verschiedene Synchronisationsmechanismen.

Sperre (Lock)

Das Lock-Objekt wird verwendet, um die exklusive Kontrolle über Ressourcen zwischen Threads zu implementieren. Während ein Thread eine Ressource sperrt, können andere Threads nicht auf diese Ressource zugreifen.

 1import threading
 2
 3lock = threading.Lock()
 4shared_resource = 0
 5
 6def worker():
 7    global shared_resource
 8    with lock:  # Acquire the lock
 9        local_copy = shared_resource
10        local_copy += 1
11        shared_resource = local_copy
12
13threads = [threading.Thread(target=worker) for _ in range(5)]
14
15for t in threads:
16    t.start()
17
18for t in threads:
19    t.join()
20
21print(f"Final value of shared resource: {shared_resource}")
  • In diesem Beispiel greifen fünf Threads auf eine gemeinsame Ressource zu, aber mit Lock wird verhindert, dass mehrere Threads die Daten gleichzeitig ändern.

Wiederentrant Sperre (RLock)

Wenn ein Thread dieselbe Sperre mehrmals erwerben muss, sollte ein RLock (wiederentrant Sperre) verwendet werden. Dies ist nützlich für rekursive Aufrufe oder Bibliotheksaufrufe, die über verschiedene Aufrufe hinweg Sperren erwerben könnten.

 1import threading
 2
 3rlock = threading.RLock()
 4shared = []
 5
 6def outer():
 7    with rlock:
 8        shared.append("outer")
 9        inner()
10
11def inner():
12    with rlock:
13        shared.append("inner")
14
15t = threading.Thread(target=outer)
16t.start()
17t.join()
18print(shared)
  • Mit RLock kann derselbe Thread eine Sperre, die er bereits hält, erneut erwerben. Dies hilft, Deadlocks bei verschachteltem Sperren zu vermeiden.

Bedingung (Condition)

Condition wird verwendet, damit Threads warten, bis eine bestimmte Bedingung erfüllt ist. Wenn ein Thread eine Bedingung erfüllt, können Sie notify() aufrufen, um einen anderen Thread zu benachrichtigen, oder notify_all(), um alle wartenden Threads zu benachrichtigen.

Unten ist ein Beispiel für einen Producer und Consumer unter Verwendung einer Condition.

 1import threading
 2
 3condition = threading.Condition()
 4shared_data = []
 5
 6def producer():
 7    with condition:
 8        shared_data.append(1)
 9        print("Produced an item")
10        condition.notify()  # Notify the consumer
11
12def consumer():
13    with condition:
14        condition.wait()  # Wait until the condition is met
15
16        item = shared_data.pop(0)
17        print(f"Consumed an item: {item}")
18
19# Create the threads
20producer_thread = threading.Thread(target=producer)
21consumer_thread = threading.Thread(target=consumer)
22
23consumer_thread.start()
24producer_thread.start()
25
26producer_thread.join()
27consumer_thread.join()
  • Dieser Code verwendet eine Condition, sodass der Producer benachrichtigt, wenn Daten hinzugefügt werden, und der Consumer auf diese Benachrichtigung wartet, bevor er die Daten abruft – so wird Synchronisation erreicht.

Daemonisierung von Threads

Daemon-Threads werden zwangsweise beendet, wenn der Hauptthread endet. Während normale Threads auf die Beendigung warten müssen, werden Daemon-Threads automatisch beendet.

 1import threading
 2import time
 3
 4def worker():
 5    while True:
 6        print("Working...")
 7        time.sleep(1)
 8
 9# Create a daemon thread
10thread = threading.Thread(target=worker)
11thread.daemon = True  # Set as a daemon thread
12
13thread.start()
14
15# Processing in the main thread
16time.sleep(3)
17print("Main thread finished")
  • In diesem Beispiel wird der worker-Thread daemonisiert, sodass er zwangsweise beendet wird, wenn der Hauptthread endet.

Thread-Verwaltung mit ThreadPoolExecutor

Neben dem threading-Modul können Sie den ThreadPoolExecutor aus dem Modul concurrent.futures verwenden, um einen Thread-Pool zu verwalten und Aufgaben parallel auszuführen.

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def worker(seconds):
 5    print(f"Sleeping for {seconds} second(s)")
 6    time.sleep(seconds)
 7    return f"Finished sleeping for {seconds} second(s)"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(worker, i) for i in range(1, 4)]
11    for future in futures:
12        print(future.result())
  • ThreadPoolExecutor erstellt einen Thread-Pool und bearbeitet Aufgaben effizient. Geben Sie die Anzahl der gleichzeitig auszuführenden Threads mit max_workers an.

Ereignisbasierte Kommunikation zwischen Threads

Mit threading.Event können Sie Flags zwischen Threads setzen, um andere Threads über das Eintreten eines Ereignisses zu benachrichtigen.

 1import threading
 2import time
 3
 4event = threading.Event()
 5
 6def worker():
 7    print("Waiting for event to be set")
 8    event.wait()  # Wait until the event is set
 9
10    print("Event received, continuing work")
11
12thread = threading.Thread(target=worker)
13thread.start()
14
15time.sleep(2)
16print("Setting the event")
17event.set()  # Set the event and notify the thread
  • Dieser Code demonstriert einen Mechanismus, bei dem der Worker-Thread auf das Event-Signal wartet und die Verarbeitung fortsetzt, wenn der Haupt-Thread event.set() aufruft.

Ausnahmebehandlung und Thread-Beendigung in Threads

Wenn Ausnahmen in Threads auftreten, werden sie nicht direkt an den Haupt-Thread weitergegeben, daher wird ein Muster benötigt, um Ausnahmen zu erfassen und weiterzuleiten.

 1import threading
 2import queue
 3
 4def worker(err_q):
 5    try:
 6        raise ValueError("Something bad")
 7    except Exception as e:
 8        err_q.put(e)
 9
10q = queue.Queue()
11t = threading.Thread(target=worker, args=(q,))
12t.start()
13t.join()
14if not q.empty():
15    exc = q.get()
16    print("Worker raised:", exc)
  • Indem Sie Ausnahmen in eine Queue einfügen und sie im Hauptthread abrufen, können Sie Fehler zuverlässig erkennen. Wenn Sie concurrent.futures.ThreadPoolExecutor verwenden, werden Ausnahmen mit future.result() erneut ausgelöst, was die Behandlung erleichtert.

Das GIL (Global Interpreter Lock) und seine Auswirkungen

Aufgrund des Mechanismus des GIL (Global Interpreter Lock) in CPython werden mehrere Python-Bytecodes nicht tatsächlich gleichzeitig im gleichen Prozess ausgeführt. Für CPU-intensive Aufgaben wie aufwändige Berechnungen wird empfohlen, multiprocessing zu verwenden. Für I/O-gebundene Aufgaben wie Dateilesen oder Netzwerkkommunikation hingegen funktioniert threading effektiv.

Zusammenfassung

Mit dem threading-Modul von Python können Sie mehrfädige Programme implementieren und mehrere Prozesse gleichzeitig ausführen. Mit Synchronisationsmechanismen wie Lock und Condition können Sie sicher auf gemeinsam genutzte Ressourcen zugreifen und komplexe Synchronisationen durchführen. Darüber hinaus wird mit der Verwendung von Daemon-Threads oder ThreadPoolExecutor die Thread-Verwaltung und effiziente parallele Verarbeitung vereinfacht.

Sie können den obigen Artikel mit Visual Studio Code auf unserem YouTube-Kanal verfolgen. Bitte schauen Sie sich auch den YouTube-Kanal an.

YouTube Video