Vollständiger Guide zu Python-Threads: Grundlagen bis Best Practices

目次

1. Einführung

Python ist eine Programmiersprache, die von vielen Entwicklern aufgrund ihrer Einfachheit und Flexibilität geschätzt wird. Unter anderem ist die Nutzung von Threads eine unverzichtbare Technik für effizientes Programmierdesign. In diesem Artikel erklären wir die Grundlagen bis hin zu fortgeschrittenen Anwendungen von Threads in Python auf verständliche Weise.

Was ist ein Thread?

Ein Thread ist eine kleine Einheit, die unabhängig innerhalb eines Programms ausgeführt wird. Durch das parallele Ausführen mehrerer Threads in einem Prozess können Tasks gleichzeitig bearbeitet werden. Dieser Mechanismus verbessert die Verarbeitungsgeschwindigkeit des Programms und ermöglicht eine effiziente Ressourcennutzung.

Warum sollte man Threads in Python lernen?

Durch die Nutzung von Threads können folgende Probleme effektiv gelöst werden.

  1. Effizienzsteigerung bei I/O-Wartezeiten
    Tasks mit vielen I/O-Operationen, wie Dateioperationen oder Netzwerkkommunikation, können durch die Verwendung von Threads die Wartezeiten verkürzen.
  2. Simultane Verarbeitung mehrerer Tasks
    Zum Beispiel ist es hilfreich, um große Datenmengen gleichzeitig zu verarbeiten oder mehrere API-Anfragen parallel zu senden.
  3. Verbesserung der Benutzererfahrung
    In GUI-Anwendungen kann die Ausführung von Prozessen im Hintergrund die Reaktionsfähigkeit der Benutzeroberfläche erhalten.

Was man in diesem Artikel lernt

In diesem Artikel werden folgende Inhalte zu Threads in Python behandelt.

  • Grundkonzepte von Threads und deren Nutzungsmethoden
  • Methoden zur Vermeidung von Datenkonflikten zwischen Threads
  • Der Mechanismus des GIL (Global Interpreter Lock) und dessen Auswirkungen
  • Methoden zur Nutzung von Threads in realen Programmen
  • Best Practices und Hinweise

Von grundlegenden Erklärungen, die auch für Anfänger leicht verständlich sind, bis hin zu praktischen Anwendungsbeispielen – alles ist umfassend abgedeckt. Für alle, die tiefer in die Thread-Operationen in Python eintauchen möchten, ist dies der ideale Leitfaden.

2. Grundlegende Konzepte von Threads

Threads sind der grundlegende Mechanismus zur Realisierung von paralleler Verarbeitung innerhalb eines Programms. In diesem Abschnitt lernen wir die Grundlagen von Threads kennen und verstehen die Unterschiede zu Prozessen und paralleler Verarbeitung.

Was ist ein Thread?

Ein Thread ist eine eigenständige Verarbeitungseinheit, die unabhängig innerhalb eines Programms arbeitet. Normalerweise wird ein Programm als Prozess ausgeführt und kann einen oder mehrere Threads enthalten.

Zum Beispiel arbeiten in einem Webbrowser folgende Threads parallel:

  • Überwachung der Benutzereingaben
  • Rendering von Webseiten
  • Streaming-Wiedergabe von Videos

Durch die Nutzung von Threads können diese Aufgaben effizient gleichzeitig ausgeführt werden.

Unterschiede zwischen Prozess und Thread

Um Threads zu verstehen, ist es zunächst notwendig, die Unterschiede zu Prozessen zu erfassen.

AspektProzessThread
Speicherplatzunabhängiginnerhalb des Prozesses geteilt
Erstellungskostenhoch (Speicher wird pro Prozess reserviert)niedrig (effizient durch Speicherfreigabe)
KommunikationsmittelIPC (Inter-Prozess-Kommunikation) erforderlichDirekte Datenfreigabe möglich
Körnung der parallelen Verarbeitunggroßklein

In Python können durch die Verwendung von Threads Ressourcen innerhalb eines Prozesses geteilt werden, während effiziente parallele Verarbeitung durchgeführt wird.

Unterschiede zwischen konkurrierender und paralleler Verarbeitung

Beim Lernen von „Threads“ ist es wichtig, die Begriffe Konkurrierende Verarbeitung (concurrent) und Parallele Verarbeitung (parallel) korrekt zu verstehen.

  • Konkurrierende Verarbeitung:
    Eine Technik, bei der Tasks abwechselnd in kleinen Schritten ausgeführt werden, um den Eindruck zu erzeugen, sie würden gleichzeitig ablaufen. Python-Threads eignen sich für konkurrierende Verarbeitung. Beispiel: Ein Verkäufer bedient mehrere Kunden nacheinander.
  • Parallele Verarbeitung:
    Eine Technik, bei der mehrere Tasks physisch gleichzeitig ausgeführt werden. Möglich, wenn mehrere CPU-Kerne vorhanden sind; in Python übernimmt hauptsächlich Multiprocessing dies. Beispiel: Mehrere Verkäufer bedienen jeweils unterschiedliche Kunden gleichzeitig.

Python-Threads sind besonders für I/O-gebundene Tasks (Dateioperationen oder Netzwerkkommunikation) in der konkurrierenden Verarbeitung geeignet.

Merkmale von Threads in Python

Python bietet als Teil der Standardbibliothek dasthreading-Modul. Mit diesem Modul können Threads einfach erstellt und verwaltet werden.

Allerdings haben Python-Threads folgende Merkmale und Einschränkungen:

  1. Existenz des Global Interpreter Lock (GIL)
    Das GIL ist ein Mechanismus, der sicherstellt, dass der Python-Interpreter nur einen Thread gleichzeitig ausführt. Daher ist der Effekt von Threads bei CPU-gebundenen Tasks (Prozessen, die viel CPU-Ressourcen verbrauchen) begrenzt.
  2. Effekt bei I/O-gebundenen Tasks
    Threads sind optimal, um I/O-Operationen wie Netzwerkkommunikation oder Datei-Ein-/Ausgabe effizienter zu gestalten.

Praktische Beispiele für die Verwendung von Threads

Hier sind Beispiele für Anwendungsfälle von Threads:

  • Web Scraping:
    Das parallele Abrufen mehrerer Webseiten.
  • Datenbankzugriff:
    Das asynchrone Verarbeiten mehrerer Anfragen von Clients.
  • Hintergrundaufgaben:
    Während der Haupthread Benutzereingaben annimmt, Ausführung schwerer Prozesse in Threads.
年収訴求

3. Erstellen von Threads in Python

Python ermöglicht es, mit dem threading-Modul Threads einfach zu erstellen und parallele Verarbeitung zu realisieren. In diesem Abschnitt erklären wir die grundlegenden Methoden zum Erstellen und Bedienen von Threads.

Übersicht über das threading-Modul

threading ist eine Standardbibliothek in Python, die zum Erstellen und Verwalten von Threads dient. Mit diesem Modul sind folgende Operationen möglich.

  • Erstellen und Starten von Threads
  • Synchronisation zwischen Threads
  • Verwaltung des Thread-Status

threading behandelt Threads als Objekte, was eine einfache und flexible Bedienung der Threads ermöglicht.

Grundlegende Methode zum Erstellen von Threads

Die gängige Methode zum Erstellen eines Threads besteht darin, die Thread-Klasse zu verwenden. Der folgende Code ist ein grundlegendes Beispiel zum Erstellen und Ausführen eines Threads.

import threading
 import time

 # Funktion, die im Thread ausgeführt wird
def print_numbers():
    for i in range(5):
        print(f"Number: {i}")
        time.sleep(1)

 # Thread erstellen
 thread = threading.Thread(target=print_numbers)

 # Thread starten
 thread.start()

 # Verarbeitung des Hauptthreads
 print("Main thread is running...")

 # Auf Thread-Ende warten
 thread.join()
 print("Thread has completed.")

Erklärung der Code-Punkte

  1. Erstellen des Threads:
    Die target-Argument der threading.Thread-Klasse wird mit der Funktion angegeben, die im Thread ausgeführt werden soll.
  2. Starten des Threads:
    Durch Aufruf der start()-Methode wird die Ausführung des Threads gestartet.
  3. Warten auf Thread-Ende:
    Die join()-Methode lässt den Hauptthread warten, bis die Verarbeitung des angegebenen Threads abgeschlossen ist.

Dieser Code führt die print_numbers-Funktion in einem separaten Thread aus, während der Hauptthread unabhängig seine Verarbeitung fortsetzt.

Übergabe von Argumenten an Threads

Wenn Parameter an einen Thread übergeben werden sollen, wird das args-Argument verwendet. Hier ist ein Beispiel.

def print_numbers_with_delay(delay):
    for i in range(5):
        print(f"Number: {i}")
        time.sleep(delay)

 # Thread mit Argumenten erstellen
 thread = threading.Thread(target=print_numbers_with_delay, args=(2,))
 thread.start()
 thread.join()

Punkte

  • Argumente werden als Tupel im Format args=(2,) übergeben.
  • In dem obigen Beispiel wird der Wert 2 als delay übergeben, was zu einer Verzögerung von 2 Sekunden zwischen den Schleifeniterationen führt.

Erstellen von Threads mit Klassen

Für fortgeschrittene Thread-Operationen kann die Thread-Klasse erweitert werden, um eine eigene Thread-Klasse zu erstellen.

class CustomThread(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        for i in range(5):
            print(f"{self.name} is running: {i}")
            time.sleep(1)

 # Instanzen der Threads erstellen
 thread1 = CustomThread(name="Thread 1")
 thread2 = CustomThread(name="Thread 2")

 # Threads starten
 thread1.start()
 thread2.start()

 # Auf Thread-Ende warten
 thread1.join()
 thread2.join()
 print("All threads have completed.")

Erklärung der Code-Punkte

  1. run-Methode:
    Die run-Methode der Thread-Klasse wird überschrieben, um die im Thread auszuführende Verarbeitung zu definieren.
  2. Benannte Threads:
    Durch das Zuweisen eines Namens zu einem Thread wird das Debuggen und Logging erleichtert.

Verwaltung des Thread-Status

Bei der Verwaltung des Thread-Status sind folgende Methoden hilfreich.

  • is_alive(): Überprüft, ob ein Thread ausgeführt wird.
  • setDaemon(True): Setzt einen Thread als Daemon- (Hintergrund-)Thread.

Beispiel für Daemon-Threads

def background_task():
    while True:
        print("Background task is running...")
        time.sleep(2)

 # Daemon-Thread erstellen
 thread = threading.Thread(target=background_task)
 thread.setDaemon(True)  # Daemon-Modus setzen
 thread.start()

 print("Main thread is exiting.")
 # Daemon-Threads enden automatisch, wenn der Hauptthread endet

Daemon-Threads enden automatisch, wenn der Hauptthread endet. Diese Eigenschaft kann genutzt werden, um Hintergrundverarbeitung zu realisieren.

4. Datensynchronisation zwischen Threads

Beim Einsatz von Threads in Python kann es zu Konflikten kommen, wenn mehrere Threads auf dieselbe Ressource zugreifen. In diesem Abschnitt wird erläutert, wie man Datakonflikte zwischen Threads durch Synchronisationsmethoden verhindert.

Was ist ein Datakonflikt zwischen Threads?

Datakonflikte zwischen Threads entstehen, wenn mehrere Threads gleichzeitig dieselbe Ressource (z. B. Variablen oder Dateien) manipulieren. Dadurch können unerwartete Ergebnisse oder Programmfehler auftreten.

Beispiel für einen Datakonflikt

import threading

counter = 0

def increment():
    global counter
    for _ in range(1000000):
        counter += 1

# Zwei Threads erstellen
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(f"Zählerwert: {counter}")

In diesem Code wird der Wert von counter gleichzeitig von zwei Threads aktualisiert, aber aufgrund von Datakonflikten erreicht er möglicherweise nicht den erwarteten Wert (2000000).

Synchronisation mit Lock

Um Datakonflikte zu verhindern, verwendet man das Lock-Objekt aus dem threading-Modul, um die Synchronisation zwischen Threads durchzuführen.

Grundlegende Verwendung von Lock

import threading

counter = 0
lock = threading.Lock()

def increment_with_lock():
    global counter
    for _ in range(1000000):
        # Lock erwerben und Verarbeitung ausführen
        with lock:
            counter += 1

# Zwei Threads erstellen
thread1 = threading.Thread(target=increment_with_lock)
thread2 = threading.Thread(target=increment_with_lock)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(f"Zählerwert mit Lock: {counter}")

Punkte des Codes

  1. with lock-Syntax:
    Mit der with-Syntax kann das Erwerben und Freigeben des Locks knapp beschrieben werden.
  2. Erwerben und Freigeben des Locks:
    Sobald ein Lock erworben wird, warten andere Threads, bis es freigegeben wird.

In diesem Code erreicht der Wert von counter durch die Verwendung des Locks das intendierte Ergebnis (2000000).

Rekursiver Lock (RLock)

Lock ist ein einfacher Lock, aber wenn derselbe Thread mehrmals einen Lock erwerben muss, verwendet man RLock (rekursiver Lock).

Beispiel für RLock

import threading

lock = threading.RLock()

def nested_function():
    with lock:
        print("Lock auf erster Ebene erworben")
        with lock:
            print("Lock auf zweiter Ebene erworben")

thread = threading.Thread(target=nested_function)
thread.start()
thread.join()

Punkte

  • RLock erlaubt es demselben Thread, mehrmals einen Lock zu erwerben.
  • Es ist nützlich für die Verwaltung verschachtelter Locks.

Synchronisation mit Semaphore

threading.Semaphore wird verwendet, um die Anzahl der nutzbaren Ressourcen zu begrenzen.

Beispiel für Semaphore

import threading
import time

semaphore = threading.Semaphore(2)

def access_resource(name):
    with semaphore:
        print(f"{name} greift auf die Ressource zu")
        time.sleep(2)
        print(f"{name} hat die Ressource freigegeben")

threads = []
for i in range(5):
    thread = threading.Thread(target=access_resource, args=(f"Thread-{i}",))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

Punkte

  • Mit einem Semaphore können nur eine angegebene Anzahl von Threads gleichzeitig auf die Ressource zugreifen.
  • In diesem Beispiel können maximal zwei Threads gleichzeitig auf die Ressource zugreifen.

Synchronisation mit Event

threading.Event ermöglicht das Senden und Empfangen von Signalen zwischen Threads.

Beispiel für Event

import threading
import time

event = threading.Event()

def wait_for_event():
    print("Thread wartet auf Event...")
    event.wait()
    print("Event wurde gesetzt. Fahre mit der Aufgabe fort.")

def set_event():
    time.sleep(2)
    print("Event setzen")
    event.set()

thread1 = threading.Thread(target=wait_for_event)
thread2 = threading.Thread(target=set_event)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

Punkte

  • wait() blockiert den Thread, bis das Event gesetzt wird.
  • Mit set() wird das Event gesetzt und wartende Threads werden fortgesetzt.

Zusammenfassung

Um Datakonflikte zwischen Threads zu verhindern, ist es wichtig, die geeignete Synchronisationsmethode auszuwählen.

  • Für einfache Synchronisation Lock verwenden
  • Bei Bedarf an verschachtelten Locks RLock verwenden
  • Um die Anzahl gleichzeitig zugreifender Threads zu begrenzen, Semaphore verwenden
  • Für den Austausch von Signalen zwischen Threads Event verwenden
年収訴求

5. GIL und die Einschränkungen der Threads

Beim Einsatz von Threads in Python ist das „GIL (Global Interpreter Lock)“ unvermeidbar. Lassen Sie uns den Mechanismus von GIL verstehen und lernen, wie man Threads angemessen unter Berücksichtigung seiner Einschränkungen einsetzt.

Was ist GIL?

GIL (Global Interpreter Lock) ist ein Lock-Mechanismus, den der Python-Interpreter (insbesondere CPython) intern verwendet. Dieses Lock beschränkt die gleichzeitige Ausführung von Python-Code in mehreren Threads.

Die Rolle von GIL

  • Es wurde eingeführt, um die Sicherheit der Speicherverwaltung zu gewährleisten.
  • Der Hauptzweck ist es, die Konsistenz von Python-Objekten (insbesondere des Referenzzählers) zu erhalten.

Allerdings kann aufgrund dieses Mechanismus die Threading in Python bei CPU-gebundenen Tasks eingeschränkt sein.

Beispiel für das Verhalten von GIL

Im folgenden Beispiel führen wir eine CPU-intensive Berechnungsaufgabe in zwei Threads aus.

import threading
 import time
 
 def cpu_bound_task():
     start = time.time()
     count = 0
     for _ in range(10**7):
         count += 1
     print(f"Aufgabe abgeschlossen in: {time.time() - start:.2f} Sekunden")
 
 # Zwei Threads erstellen
 thread1 = threading.Thread(target=cpu_bound_task)
 thread2 = threading.Thread(target=cpu_bound_task)
 
 start_time = time.time()
 
 thread1.start()
 thread2.start()
 
 thread1.join()
 thread2.join()
 
 print(f"Gesamtzeit: {time.time() - start_time:.2f} Sekunden")

Analyse des Ergebnisses

Wenn Sie diesen Code ausführen, wird die Verarbeitungszeit trotz der Verwendung von Threads nicht doppelt so schnell. Das liegt daran, dass GIL die gleichzeitige Ausführung der Threads behindert.

Situationen, in denen GIL Einfluss hat

  1. CPU-gebundene Tasks
    Bei Tasks wie numerischen Berechnungen oder Bildverarbeitung, die die CPU intensiv nutzen, wird GIL zum Engpass, und die Vorteile von Threads werden kaum genutzt.
  2. I/O-gebundene Tasks
    Bei Tasks mit viel Wartezeit wie Dateioperationen oder Netzwerkkommunikation hat GIL wenig Einfluss, und die Vorteile von Threads können genutzt werden.

Wie man die Einschränkungen von GIL überwindet

1. Verwendung von Multiprocessing

Als Methode, die nicht von GIL beeinflusst wird, kann man Multiprocessing verwenden.Mit dem multiprocessing-Modul können mehrere Prozesse erstellt werden, um parallele Verarbeitung zu realisieren.

Das Folgende ist ein Beispiel, wie CPU-gebundene Tasks mit Multiprocessing verarbeitet werden.

from multiprocessing import Process
 import time
 
 def cpu_bound_task():
     start = time.time()
     count = 0
     for _ in range(10**7):
         count += 1
     print(f"Aufgabe abgeschlossen in: {time.time() - start:.2f} Sekunden")
 
 # Zwei Prozesse erstellen
 process1 = Process(target=cpu_bound_task)
 process2 = Process(target=cpu_bound_task)
 
 start_time = time.time()
 
 process1.start()
 process2.start()
 
 process1.join()
 process2.join()
 
 print(f"Gesamtzeit: {time.time() - start_time:.2f} Sekunden")

Hinweise

  • Jeder Prozess hat einen unabhängigen Speicherraum, daher wird GIL nicht beeinflusst.
  • Bei CPU-gebundenen Tasks ist die Verwendung von Prozessen effizienter als Threads.

2. Verwendung von C-Erweiterungsmodulen

Python-C-Erweiterungsmodule (z. B. NumPy oder Pandas) können intern das GIL freigeben und die Verarbeitung parallelisieren. Dadurch verbessert sich die Leistung bei CPU-gebundenen Tasks.

Beispiel:

  • NumPy verwenden, um numerische Berechnungen zu beschleunigen.
  • Python-Code mit Cython oder Numba kompilieren und optimieren.

3. asyncio nutzen

Bei I/O-gebundenen Tasks kann man anstelle von Threads asyncio nutzen, um effiziente parallele Verarbeitung in einem Single-Thread zu realisieren.

Beispiel:

import asyncio
 
 async def io_bound_task(name, delay):
     print(f"{name} gestartet")
     await asyncio.sleep(delay)
     print(f"{name} abgeschlossen")
 
 async def main():
     await asyncio.gather(
         io_bound_task("Task 1", 2),
         io_bound_task("Task 2", 3)
     )
 
 asyncio.run(main())

Hinweise

  • asyncio ist asynchron, daher weniger von GIL beeinflusst.
  • Es eignet sich für I/O-zentrierte Verarbeitungen wie Netzwerkkommunikation oder Dateioperationen.

Vorteile und Nachteile von GIL

Vorteile

  • Vereinfacht die Speicherverwaltung in Python.
  • Verbessert die Datensicherheit in Single-Thread-Umgebungen.

Nachteile

  • Die Leistungssteigerung bei Multithreading ist begrenzt.
  • Bei CPU-gebundenen Tasks muss man Prozesse verwenden.

Zusammenfassung

GIL ist ein großer Einschränkungsfaktor für Threads in Python, aber indem man seinen Einfluss versteht und geeignete Methoden wählt, kann man das Problem überwinden.

  • CPU-gebundene Tasks: Multiprocessing oder C-Erweiterungsmodule nutzen.
  • I/O-gebundene Tasks: Threads oder asyncio verwenden.

6. Praktisches Beispiel: Programme mit der Nutzung von Threads

Threads können, wenn sie angemessen genutzt werden, komplexe Aufgaben effizient verarbeiten. In diesem Abschnitt stellen wir mehrere konkrete praktische Beispiele vor, die Python-Threads verwenden.

1. Paralleles Scraping mehrerer Webseiten

Beim Web-Scraping kann durch die Verwendung von Threads beim Abrufen von Daten aus mehreren Seiten die Verarbeitungszeit verkürzt werden.

import threading
 import requests
 
 def fetch_url(url):
     response = requests.get(url)
     print(f"{url} abgerufen: {len(response.content)} Bytes")
 
 urls = [
     "https://example.com",
     "https://httpbin.org",
     "https://www.python.org",
 ]
 
 threads = []
 
 # Erstellen eines Threads für jede URL
 for url in urls:
     thread = threading.Thread(target=fetch_url, args=(url,))
     threads.append(thread)
     thread.start()
 
 # Warten auf den Abschluss aller Threads
 for thread in threads:
     thread.join()
 
 print("Alle URLs abgerufen.")

Punkte

  • Mehrere URLs werden parallel mit Threads abgerufen.
  • HTTP-Anfragen können einfach mit der requests-Bibliothek gesendet werden.

2. Simultanes Lesen und Schreiben von Dateien

Durch die Verwendung von Threads kann die Verarbeitung zum gleichzeitigen Lesen und Schreiben einer großen Anzahl von Dateien effizienter gestaltet werden.

import threading
 
 def write_to_file(filename, content):
     with open(filename, 'w') as f:
         f.write(content)
     print(f"In {filename} geschrieben")
 
 files = [
     ("file1.txt", "Inhalt für Datei 1"),
     ("file2.txt", "Inhalt für Datei 2"),
     ("file3.txt", "Inhalt für Datei 3"),
 ]
 
 threads = []
 
 # Erstellen eines Threads für jede Datei
 for filename, content in files:
     thread = threading.Thread(target=write_to_file, args=(filename, content))
     threads.append(thread)
     thread.start()
 
 # Warten auf den Abschluss aller Threads
 for thread in threads:
     thread.join()
 
 print("Alle Dateien geschrieben.")

Punkte

  • Jedes Thread schreibt in eine unabhängige Datei und beschleunigt die Verarbeitung.
  • Es ist wirksam, wenn mehrere Threads gleichzeitig auf unterschiedliche Ressourcen zugreifen.

3. Hintergrundverarbeitung in GUI-Anwendungen

In GUI-Anwendungen kann Threads verwendet werden, um schwere Aufgaben im Hintergrund auszuführen, während der Hauptthread die Benutzeroberfläche verarbeitet.

Hier ist ein einfaches Beispiel mit tkinter.

import threading
 import time
 from tkinter import Tk, Button, Label
 
 def long_task(label):
     label.config(text="Aufgabe gestartet...")
     time.sleep(5)  # Simulation einer langwierigen Verarbeitung
     label.config(text="Aufgabe abgeschlossen!")
 
 def start_task(label):
     thread = threading.Thread(target=long_task, args=(label,))
     thread.start()
 
 # Einrichtung der GUI
 root = Tk()
 root.title("Beispiel für GUI mit Threads")
 
 label = Label(root, text="Klicken Sie auf die Schaltfläche, um die Aufgabe zu starten.")
 label.pack(pady=10)
 
 button = Button(root, text="Aufgabe starten", command=lambda: start_task(label))
 button.pack(pady=10)
 
 root.mainloop()

Punkte

  • Durch die Verwendung von Threads für die Hintergrundverarbeitung wird verhindert, dass die UI des Hauptthreads blockiert wird.
  • Aufgaben werden asynchron mit threading.Thread ausgeführt.

4. Echtzeit-Datenverarbeitung

Beim Echtzeit-Verarbeiten von Daten aus Sensoren oder Logdateien ist es möglich, Threads zu verwenden, um parallel zu verarbeiten.

import threading
 import time
 import random
 
 def process_data(sensor_name):
     for _ in range(5):
         data = random.randint(0, 100)
         print(f"{sensor_name} Daten gelesen: {data}")
         time.sleep(1)
 
 sensors = ["Sensor-1", "Sensor-2", "Sensor-3"]
 
 threads = []
 
 # Erstellen eines Threads für jeden Sensor
 for sensor in sensors:
     thread = threading.Thread(target=process_data, args=(sensor,))
     threads.append(thread)
     thread.start()
 
 # Warten auf den Abschluss aller Threads
 for thread in threads:
     thread.join()
 
 print("Alle Sensordaten verarbeitet.")

Punkte

  • Jedes Thread verarbeitet Daten eines unabhängigen Sensors.
  • Es eignet sich für die Simulation der Echtzeit-Datensammlung und -analyse.

Zusammenfassung

Durch diese praktischen Beispiele haben wir Methoden zum effizienten Programmierdesign mit Python-Threads gelernt.

  • Web-Scraping für den Datenerwerb
  • Dateibetrieb für die Beschleunigung
  • GUI-Anwendungen für die Hintergrundverarbeitung
  • Echtzeit-Datenverarbeitung für die parallele Verarbeitung

Threads sind ein mächtiges Tool, aber es ist wichtig, sie angemessen zu gestalten, um Datenkonflikte und Deadlocks zu vermeiden.

7. Best Practices beim Einsatz von Threads

Threads sind ein mächtiges Werkzeug zur Effizienzsteigerung paralleler Verarbeitung, aber bei falscher Verwendung können Probleme wie Deadlocks oder Datenkonflikte auftreten. In diesem Abschnitt stellen wir Best Practices vor, die beim Einsatz von Threads in Python beachtet werden sollten.

1. Vermeidung von Deadlocks

Ein Deadlock tritt auf, wenn mehrere Threads gegenseitig auf Locks warten. Um diese Situation zu vermeiden, ist es wichtig, die Reihenfolge und Methode des Erwerbs von Locks zu vereinheitlichen.

Beispiel für einen Deadlock

import threading
 import time

lock1 = threading.Lock()
lock2 = threading.Lock()

def thread1_task():
    with lock1:
        print("Thread 1 acquired lock1")
        time.sleep(1)
        with lock2:
            print("Thread 1 acquired lock2")

def thread2_task():
    with lock2:
        print("Thread 2 acquired lock2")
        time.sleep(1)
        with lock1:
            print("Thread 2 acquired lock1")

thread1 = threading.Thread(target=thread1_task)
thread2 = threading.Thread(target=thread2_task)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

In diesem Code geraten lock1 und lock2 in einen gegenseitigen Wartezustand und es tritt ein Deadlock auf.

Lösungsmethoden

  1. Reihenfolge des Lock-Erwerbs vereinheitlichen: Vereinheitlichen Sie die Reihenfolge, in der Locks in allen Threads erworben werden.
  2. Timeout einstellen: Stellen Sie ein Timeout für den Lock-Erwerb ein und unterbrechen Sie die Verarbeitung, falls es innerhalb einer bestimmten Zeit nicht erworben werden kann.
lock1.acquire(timeout=1)

2. Optimierung der Thread-Anzahl

Wenn die Anzahl der Threads unbegrenzt erhöht wird, entsteht Overhead und die Performance sinkt. Wählen Sie zur Einstellung einer angemessenen Thread-Anzahl die optimale Zahl je nach Art der Aufgabe.

Allgemeine Richtlinien

  • I/O-gebundene Aufgaben: Thread-Anzahl höher setzen (z. B. mehr als das Doppelte der üblichen CPU-Kerne).
  • CPU-gebundene Aufgaben: Auf die Anzahl der CPU-Kerne oder weniger setzen.

3. Sichere Behandlung des Thread-Endes

Das sichere Beenden von Threads ist wichtig, um die Integrität des Programms zu wahren. Das threading-Modul hat keine Funktion zum erzwungenen Beenden von Threads, daher muss die Beendigungsbedingung innerhalb des Threads verwaltet werden.

Beispiel für sicheres Thread-Ende

import threading
 import time

class SafeThread(threading.Thread):
    def __init__(self):
        super().__init__()
        self._stop_event = threading.Event()

    def run(self):
        while not self._stop_event.is_set():
            print("Thread is running...")
            time.sleep(1)

    def stop(self):
        self._stop_event.set()

thread = SafeThread()
thread.start()

time.sleep(5)
thread.stop()
thread.join()
print("Thread has been safely stopped.")

Punkte

  • Verwenden Sie Flags oder Events innerhalb des Threads, um den Beendigungsstatus zu überwachen.
  • Verwenden Sie die stop()-Methode, um die Beendigungsbedingung explizit zu setzen.

4. Debugging mit Logs

Um das Verhalten in Threads zu verfolgen, nutzen Sie das logging-Modul. Durch die Verwendung von logging statt print-Anweisungen können detaillierte Informationen inklusive Thread-Namen und Zeitstempel aufgezeichnet werden.

Beispiel für Log-Konfiguration

import threading
 import logging

logging.basicConfig(level=logging.DEBUG, format='%(threadName)s: %(message)s')

def task():
    logging.debug("Task started")
    logging.debug("Task completed")

thread = threading.Thread(target=task, name="MyThread")
thread.start()
thread.join()

Punkte

  • Setzen Sie den Thread-Namen explizit, um die Lesbarkeit der Logs zu verbessern.
  • Nutzen Sie Log-Level (DEBUG, INFO, WARNING usw.), um die Wichtigkeit zu unterscheiden.

5. Threads und asynchrone Verarbeitung je nach Anwendungsfall unterscheiden

Threads eignen sich für I/O-gebundene Aufgaben, aber in manchen Fällen ist asynchrone Verarbeitung mit asyncio effizienter. Berücksichtigen Sie die folgenden Kriterien für die Unterscheidung.

  • Fälle, in denen Threads geeignet sind:
  • Hintergrundverarbeitung in GUI-Anwendungen
  • Operationen mit Daten, die mit anderen Threads oder Prozessen geteilt werden
  • Fälle, in denen asynchrone Verarbeitung geeignet ist:
  • Wenn eine große Anzahl von I/O-Aufgaben effizient verarbeitet werden soll
  • Wenn keine komplexe Zustandsverwaltung erforderlich ist

6. Einfaches Design anstreben

Die übermäßige Verwendung von Threads macht den Code oft komplex. Achten Sie auf die folgenden Punkte, um ein einfaches und wartbares Design zu verfolgen.

  • Beschränken Sie Threads auf das Nötigste.
  • Machen Sie die Rollen der Threads klar.
  • Minimieren Sie die Datenfreigabe zwischen Threads und verwenden Sie bei Möglichkeit Queues.

8. Zusammenfassung

Die Threads in Python sind ein mächtiges Werkzeug, um die Effizienz von Programmen zu verbessern. In diesem Artikel haben wir von den Grundlagen der Threads über fortgeschrittene Anwendungen bis hin zu Vorsichtsmaßnahmen alles erklärt. Hier werfen wir einen Blick zurück auf den Inhalt und bestätigen die Punkte, die man im Sinn behalten sollte, wenn man Threads nutzt.

Die wichtigsten Punkte dieses Artikels

  1. Grundlegende Konzepte von Threads
  • Threads sind kleine Einheiten, die unabhängig innerhalb eines Prozesses arbeiten und parallele Verarbeitung ermöglichen.
  • Es ist wichtig, den Unterschied zwischen paralleler Verarbeitung (concurrent) und gleichzeitiger Verarbeitung (parallel) zu verstehen und je nach Anwendungsfall zu unterscheiden.
  1. Erstellung von Threads in Python
  • Mit dem threading-Modul können Threads einfach erstellt werden.
  • Mit den Methoden start() und join() der Thread-Klasse können Threads gesteuert werden.
  • Durch die Erstellung einer benutzerdefinierten Klasse wird eine flexible Thread-Steuerung möglich.
  1. Synchronisation zwischen Threads
  • Um Datenkonflikte zu vermeiden, werden Synchronisationsobjekte wie Lock, RLock oder Semaphore genutzt.
  • Durch die Verwendung von Events und Timeouts wird die Steuerung zwischen Threads weiter verfeinert.
  1. Einfluss des GIL (Global Interpreter Lock)
  • Durch das GIL sind Python-Threads bei CPU-gebundenen Aufgaben eingeschränkt.
  • Für CPU-gebundene Aufgaben wird Multiprocessing empfohlen, für I/O-gebundene Aufgaben Threads.
  1. Praktische Beispiele
  • Wir haben konkrete Anwendungsfälle für Threads wie Web-Scraping, Dateioperationen und Hintergrundverarbeitung in GUI-Anwendungen vorgestellt.
  1. Best Practices
  • Durch das Vermeiden von Deadlocks, die Einstellung der richtigen Anzahl von Threads, sichere Beendigungsprozesse und die Nutzung von Logs wird die Effizienz der Threads und die Stabilität des Programms verbessert.

Grundsätze beim Einsatz von Threads

  • Threads sind nicht allmächtig
    Threads sind ein sehr nützliches Werkzeug, aber bei falscher Anwendung kann die Leistung abnehmen. Es ist wichtig, sie im richtigen Szenario zu verwenden.
  • Das Design einfach halten
    Die übermäßige Nutzung von Threads erhöht die Komplexität des Codes. Definieren Sie klare Rollen und halten Sie die Synchronisation einfach, um die Wartbarkeit zu verbessern.
  • Andere Optionen in Betracht ziehen
    Statt Threads können asyncio oder multiprocessing in manchen Fällen geeigneter sein. Wählen Sie die optimale Methode je nach Eigenschaften der Aufgabe.

Nächste Schritte

Nachdem Sie die Grundlagen von Threads gelernt haben, vertiefen Sie bitte die folgenden Themen.

  1. Asynchrone Programmierung
  • Lernen Sie das asyncio-Modul von Python kennen und erwerben Sie Kenntnisse darüber, wie effiziente asynchrone Verarbeitung mit einem einzelnen Thread realisiert werden kann.
  1. Multiprocessing
  • Vermeiden Sie die Einschränkungen des GIL und optimieren Sie die parallele Verarbeitung für CPU-gebundene Aufgaben.
  1. Fortgeschrittene Thread-Steuerung
  • Nutzen Sie Thread-Pools (concurrent.futures.ThreadPoolExecutor) und Debugging-Tools, um die Thread-Verwaltung effizienter zu gestalten.
  1. Anwendung in realen Szenarien
  • Arbeiten Sie an Projekten mit Threads (z. B. Web-Crawler, Echtzeit-Datenverarbeitung), um praktische Fähigkeiten zu erwerben.

Zum Abschluss

Die Threads in Python können, wenn sie richtig designed und verwaltet werden, eine leistungsstarke parallele Verarbeitung ermöglichen. Nutzen Sie das in diesem Artikel Gelernte, um effizientere und stabilere Programme zu erstellen.

侍エンジニア塾