Python3 Multithreading

Multithreading ähnelt der gleichzeitigen Ausführung mehrerer verschiedener Programme. Das Ausführen von Multithreading hat die folgenden Vorteile:

  • Durch die Verwendung von Threads können Aufgaben, die in einem Programm viel Zeit beanspruchen, im Hintergrund verarbeitet werden.
  • Die Benutzeroberfläche kann ansprechender sein. Wenn ein Benutzer beispielsweise auf eine Schaltfläche klickt, um die Behandlung bestimmter Ereignisse auszulösen, kann ein Fortschrittsbalken eingeblendet werden, der den Fortschritt der Verarbeitung anzeigt.
  • Die Ausführungsgeschwindigkeit des Programms kann zunehmen.
  • Threads sind nützlicher für die Implementierung einiger Warteaufgaben wie Benutzereingabe, Dateilesen und -schreiben sowie Senden und Empfangen von Netzwerkdaten. In solchen Fällen können wir einige wertvolle Ressourcen wie die Speichernutzung freigeben.

Jeder unabhängige Thread hat einen Einstiegspunkt für die Programmausführung, eine sequentielle Ausführungsreihenfolge und einen Endpunkt für das Programm. Threads können jedoch nicht unabhängig ausgeführt werden; sie müssen von der Anwendung abhängen, die die Steuerung für die Ausführung mehrerer Threads bereitstellt.

Jeder Thread hat seinen eigenen Satz von CPU-Registern, der als Kontext des Threads bezeichnet wird und den Zustand der CPU-Register widerspiegelt, als der Thread zuletzt ausgeführt wurde.

Die Befehlszeiger- und Stapelzeiger-Register sind die beiden wichtigsten Register im Thread-Kontext. Threads laufen immer in dem vom Prozess erhaltenen Kontext, und diese Adressen werden verwendet, um Speicher im Adressraum des Prozesses zu markieren, dem der Thread gehört.

  • Threads können verdrängt (unterbrochen) werden.
  • Während andere Threads ausgeführt werden, kann ein Thread vorübergehend zurückgestellt werden (auch als Schlafen bezeichnet) – dies ist das Thread-Yielding.

Threads können unterteilt werden in:

  • Kernel-Threads:Vom Betriebssystemkern erstellt und beendet.
  • Benutzer-Threads:Threads, die in Benutzerprogrammen implementiert werden, ohne Kernel-Unterstützung zu erfordern.

Zwei häufig verwendete Module für Threads in Python3 sind:

  • _thread
  • threading (empfohlen)

Das Modul thread ist veraltet. Benutzer können stattdessen das Modul threading verwenden. Daher kann in Python3 das Modul "thread" nicht mehr verwendet werden. Aus Kompatibilitätsgründen wurde thread in Python3 zu "_thread" umbenannt.


Beginne, Python-Threads zu lernen

Es gibt zwei Möglichkeiten, Threads in Python zu verwenden: Funktionen oder die Verwendung von Klassen zum Kapseln von Thread-Objekten.

Funktionaler Stil: Rufe die Funktion start_new_thread() im Modul _thread auf, um einen neuen Thread zu erzeugen. Die Syntax lautet wie folgt:

_thread.start_new_thread ( function, args[, kwargs] )

Parameter description:

  • function - Die Thread-Funktion.
  • args - Die Parameter, die an die Thread-Funktion übergeben werden; es muss vom Typ Tupel sein.
  • kwargs - Optionale Parameter.

Beispiel

#!/usr/bin/python3

import _thread
import time

# Definiere eine Funktion für den Thread
def print_time( threadName, delay):
   count = 0
   while count < 5:
      time.sleep(delay)
      count += 1
      print ("%s: %s" % ( threadName, time.ctime(time.time()) ))

# Zwei Threads erstellen
try:
   _thread.start_new_thread( print_time, ("Thread-1", 2, ) )
   _thread.start_new_thread( print_time, ("Thread-2", 4, ) )
except:
   print ("Fehler: Thread kann nicht gestartet werden")

while 1:
   pass

Die Ausgabe nach Ausführung des obigen Programms ist wie folgt:

Thread-1: Wed Jan  5 17:38:08 2022
Thread-2: Wed Jan  5 17:38:10 2022
Thread-1: Wed Jan  5 17:38:10 2022
Thread-1: Wed Jan  5 17:38:12 2022
Thread-2: Wed Jan  5 17:38:14 2022
Thread-1: Wed Jan  5 17:38:14 2022
Thread-1: Wed Jan  5 17:38:16 2022
Thread-2: Wed Jan  5 17:38:18 2022
Thread-2: Wed Jan  5 17:38:22 2022
Thread-2: Wed Jan  5 17:38:26 2022

Nach Ausführung des obigen Programms können Sie ctrl-c drücken, um zu beenden.


Thread-Modul

Python3 unterstützt Threads durch zwei Standardbibliotheken: _thread und threading.

_thread bietet Low-Level- und primitive Threads und eine einfache Sperre. Die Funktionalität ist im Vergleich zum threading-Modul noch relativ begrenzt.

Zusätzlich zu allen Methoden im _thread-Modul bietet das threading-Modul außerdem die folgenden Methoden:

  • threading.current_thread(): Gibt die aktuelle Thread-Variable zurück.
  • threading.enumerate(): Gibt eine Liste mit allen laufenden Threads zurück. "Laufend" bedeutet, nachdem der Thread gestartet wurde und bevor er beendet wurde; Threads vor dem Start oder nach der Beendigung sind nicht enthalten.
  • threading.active_count(): Gibt die Anzahl der laufenden Threads zurück, mit demselben Ergebnis wie len(threading.enumerate()).
  • threading.Thread(target, args=(), kwargs={}, daemon=None):
    • ErstelleThreadInstanz der Klasse.
    • target: Die Zielfunktion, die der Thread ausführen soll.
    • args: Die Parameter der Zielfunktion, als Tupel übergeben.
    • kwargs: Die Schlüsselwortargumente der Zielfunktion, als Dictionary übergeben.
    • daemon: Gibt an, ob der Thread ein Daemon-Thread ist.

Die Klasse threading.Thread bietet die folgenden Methoden und Attribute:

  1. __init__(self, group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None):

    • InitialisierungThreadObjekt.
    • group: Thread-Gruppe, derzeit ungenutzt, für zukünftige Erweiterungen reserviert.
    • target: Die Zielfunktion, die der Thread ausführen soll.
    • name: Der Name des Threads.
    • args: Die Parameter der Zielfunktion, als Tupel übergeben.
    • kwargs: Die Schlüsselwortargumente der Zielfunktion, als Dictionary übergeben.
    • daemon: Gibt an, ob der Thread ein Daemon-Thread ist.
  2. start(self):

    • Startet den Thread. Er ruft die Methode des Threads aufrun()Methode.
  3. run(self):

    • Der Thread definiert den Code, der in dieser Methode ausgeführt wird.
  4. join(self, timeout=None):

    • Wartet darauf, dass der Thread beendet wird. Standardmäßigjoin()blockiert er, bis der aufgerufene Thread beendet wird. Wenn dertimeoutParameter angegeben ist, wartet er höchstenstimeoutSekunden.
  5. is_alive(self):

    • Gibt zurück, ob der Thread läuft. Wenn der Thread gestartet wurde und noch nicht beendet ist, gibt erTrue, andernfalls gibt sie zurückFalse。
  6. getName(self):

    • Gibt den Namen des Threads zurück.
  7. setName(self, name):

    • Legt den Namen des Threads fest.
  8. identAttribute:

    • Die eindeutige Kennung des Threads.
  9. daemonAttribute:

    • Das Daemon-Flag des Threads, das angibt, ob es sich um einen Daemon-Thread handelt.
  10. isDaemon()Methoden:

Ein einfaches Thread-Beispiel:

Beispiel

import threading
import time

def print_numbers():
    for i in range(5):
        time.sleep(1)
        print(i)

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

# Thread starten
thread.start()

# Warten, bis der Thread beendet ist
thread.join()

Das Ausgabeergebnis ist:

0
1
2
3
4

Erstellen von Threads mit dem threading-Modul

Wir können eine neue Unterklasse erstellen, indem wir direkt von threading.Thread erben, und nach der Instanziierung die Methode start() aufrufen, um den neuen Thread zu starten. Das heißt, sie ruft die Methode run() des Threads auf:

Beispiel

#!/usr/bin/python3

import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, delay):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.delay = delay
    def run(self):
        print ("Thread starten:" + self.name)
        print_time(self.name, self.delay, 5)
        print ("Thread beenden:" + self.name)

def print_time(threadName, delay, counter):
    while counter:
        if exitFlag:
            threadName.exit()
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

# Einen neuen Thread erstellen
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# Den neuen Thread starten
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("Hauptthread beenden")

Das Ausführungsergebnis des obigen Programms ist wie folgt;

开始线程:Thread-1
开始线程:Thread-2
Thread-1: Wed Jan  5 17:34:54 2022
Thread-2: Wed Jan  5 17:34:55 2022
Thread-1: Wed Jan  5 17:34:55 2022
Thread-1: Wed Jan  5 17:34:56 2022
Thread-2: Wed Jan  5 17:34:57 2022
Thread-1: Wed Jan  5 17:34:57 2022
Thread-1: Wed Jan  5 17:34:58 2022
退出线程:Thread-1
Thread-2: Wed Jan  5 17:34:59 2022
Thread-2: Wed Jan  5 17:35:01 2022
Thread-2: Wed Jan  5 17:35:03 2022
退出线程:Thread-2
退出主线程

Thread-Synchronisation

Wenn mehrere Threads gemeinsam ein bestimmtes Datenelement ändern, können unerwartete Ergebnisse auftreten. Um die Korrektheit der Daten zu gewährleisten, müssen mehrere Threads synchronisiert werden.

Die Verwendung von Lock und Rlock von Thread-Objekten kann eine einfache Thread-Synchronisierung erreichen. Beide Objekte verfügen über die Methoden acquire und release. Für Daten, auf die jeweils nur ein Thread zugreifen darf, können die Operationen zwischen den Methoden acquire und release platziert werden. Wie folgt:

Der Vorteil von Multithreading besteht darin, dass mehrere Aufgaben gleichzeitig ausgeführt werden können (zumindest fühlt es sich so an). Wenn Threads jedoch Daten gemeinsam nutzen müssen, kann ein Problem der Datenunsynchronisierung auftreten.

Betrachten Sie eine Situation: Alle Elemente in einer Liste sind 0. Der "set"-Thread ändert alle Elemente von hinten nach vorne in 1, während der "print"-Thread die Liste von vorne nach hinten liest und ausgibt.

Dann ist es möglich, dass, wenn der "set"-Thread mit dem Ändern beginnt, der "print"-Thread die Liste ausgibt und die Ausgabe zur Hälfte aus 0 und zur Hälfte aus 1 besteht. Dies ist Datenunsynchronisierung. Um diese Situation zu vermeiden, wird das Konzept der Sperren eingeführt.

Eine Sperre hat zwei Zustände – gesperrt und entsperrt. Wann immer ein Thread, wie z. B. "set", auf gemeinsam genutzte Daten zugreifen möchte, muss er zuerst die Sperre erwerben; wenn ein anderer Thread, wie z. B. "print", die Sperre bereits erworben hat, wird der "set"-Thread pausiert, was eine synchrone Blockierung ist; nachdem der "print"-Thread den Zugriff beendet und die Sperre freigegeben hat, fährt der "set"-Thread fort.

Mit einer solchen Verarbeitung werden beim Ausgeben der Liste entweder nur 0 oder nur 1 ausgegeben, und die peinliche Szene von halb 0 und halb 1 tritt nicht mehr auf.

Beispiel

#!/usr/bin/python3

import threading
import time

class myThread (threading.Thread):
    def __init__(self, threadID, name, delay):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.delay = delay
    def run(self):
        print ("Thread starten: " + self.name)
        # Sperre für die Thread-Synchronisierung erwerben
        threadLock.acquire()
        print_time(self.name, self.delay, 3)
        # Sperre freigeben, um den nächsten Thread zu starten
        threadLock.release()

def print_time(threadName, delay, counter):
    while counter:
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

threadLock = threading.Lock()
threads = []

# Einen neuen Thread erstellen
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# Den neuen Thread starten
thread1.start()
thread2.start()

# Threads zur Thread-Liste hinzufügen
threads.append(thread1)
threads.append(thread2)

# Warten, bis alle Threads abgeschlossen sind
for t in threads:
    t.join()
print ("Hauptthread beenden")

Führen Sie das obige Programm aus, und das Ausgabeergebnis ist:

开启线程: Thread-1
开启线程: Thread-2
Thread-1: Wed Jan  5 17:36:50 2022
Thread-1: Wed Jan  5 17:36:51 2022
Thread-1: Wed Jan  5 17:36:52 2022
Thread-2: Wed Jan  5 17:36:54 2022
Thread-2: Wed Jan  5 17:36:56 2022
Thread-2: Wed Jan  5 17:36:58 2022
退出主线程

Thread-Prioritätswarteschlange (Queue)

Das Queue-Modul von Python bietet synchronisierte, threadsichere Warteschlangenklassen, darunter die FIFO-Warteschlange (First-In, First-Out) Queue, die LIFO-Warteschlange (Last-In, First-Out) LifoQueue und die Prioritätswarteschlange PriorityQueue.

Diese Warteschlangen implementieren alle Lock-Primitive und können direkt in Multithreading verwendet werden. Warteschlangen können verwendet werden, um die Synchronisation zwischen Threads zu erreichen.

Gängige Methoden im Queue-Modul:

  • Queue.qsize() gibt die Größe der Warteschlange zurück.
  • Queue.empty() Wenn die Warteschlange leer ist, gibt True zurück; andernfalls False
  • Queue.full() Wenn die Warteschlange voll ist, gibt True zurück; andernfalls False
  • Queue.full entspricht der Größe von maxsize
  • Queue.get([block[, timeout]]) Ruft eine Queue ab; timeout ist die Wartezeit
  • Queue.get_nowait() Entspricht Queue.get(False)
  • Queue.put(item) Schreibt in die Queue; timeout ist die Wartezeit
  • Queue.put_nowait(item) Entspricht Queue.put(item, False)
  • Queue.task_done() Nach Abschluss einer Aufgabe sendet die Funktion Queue.task_done() ein Signal an die Queue, dass die Aufgabe abgeschlossen wurde
  • Queue.join() Bedeutet eigentlich, zu warten, bis die Queue leer ist, und dann andere Operationen auszuführen

Beispiel

#!/usr/bin/python3

import queue
import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, q):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.q = q
    def run(self):
        print ("Starte Thread:" + self.name)
        process_data(self.name, self.q)
        print ("Beende Thread:" + self.name)

def process_data(threadName, q):
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            data = q.get()
            queueLock.release()
            print ("%s verarbeitet %s" % (threadName, data))
        else:
            queueLock.release()
        time.sleep(1)

threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["Eins", "Zwei", "Drei", "Vier", "Fünf"]
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1

# Einen neuen Thread erstellen
for tName in threadList:
    thread = myThread(threadID, tName, workQueue)
    thread.start()
    threads.append(thread)
    threadID += 1

# Die Warteschlange füllen
queueLock.acquire()
for word in nameList:
    workQueue.put(word)
queueLock.release()

# Warten, bis die Warteschlange geleert ist
while not workQueue.empty():
    pass

# Den Thread benachrichtigen, dass es Zeit zum Beenden ist
exitFlag = 1

# Warten, bis alle Threads abgeschlossen sind
for t in threads:
    t.join()
print ("Hauptthread wird beendet")

Das Ergebnis der Ausführung des obigen Programms:

开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-3
Thread-3 processing One
Thread-1 processing Two
Thread-2 processing Three
Thread-3 processing Four
Thread-1 processing Five
退出线程:Thread-3
退出线程:Thread-2
退出线程:Thread-1
退出主线程
Weitere Erweiterungen