Es gibt einige grundlegende Konzepte, die bei der Multithread-Programmierung verstanden werden müssen und die für alle Programmiersprachen gelten. Inhalt:
Gleichzeitige Programmierung
Multitasking-Betriebssystem
Multi-Threading vs. Multiprozess
Thread-Sicherheit
Thread-Lebensdauer Zyklus
Arten von Threads
Gleichzeitige Programmierung
Verschiedene Programmierparadigmen haben unterschiedliche Perspektiven auf Software. Die gleichzeitige Programmierung betrachtet Software als eine Kombination aus Aufgaben und Ressourcen – Aufgaben konkurrieren und teilen Ressourcen, führen Aufgaben aus, wenn die Ressourcen erfüllt sind, andernfalls warten sie auf Ressourcen.
Die gleichzeitige Programmierung erleichtert das Verständnis und die Wiederverwendung von Software und kann in bestimmten Szenarien die Leistung erheblich verbessern.
Multitasking-Betriebssystem
Um Parallelität zu erreichen, benötigen Sie zunächst die Unterstützung des Betriebssystems. Die meisten heutigen Betriebssysteme sind Multitasking-Betriebssysteme, die mehrere Aufgaben „gleichzeitig“ ausführen können.
Multitasking kann auf Prozess- oder Thread-Ebene durchgeführt werden.
Ein Prozess bezieht sich auf eine Anwendung, die im Speicher ausgeführt wird. Jeder Prozess verfügt über seinen eigenen unabhängigen Speicherbereich. Ein Multitasking-Betriebssystem kann diese Prozesse „gleichzeitig“ ausführen.
Threads beziehen sich auf Codeblöcke, die nicht in der richtigen Reihenfolge sind und mehrmals in einem Prozess ausgeführt werden. Mehrere Threads können „gleichzeitig“ ausgeführt werden, sodass mehrere Threads als „gleichzeitig“ betrachtet werden. Der Zweck von Multithreading besteht darin, die Nutzung der CPU-Ressourcen zu maximieren. Beispielsweise laufen in einem JVM-Prozess alle Programmcodes in Threads ab.
Die „Gleichzeitigkeit“ und „Parallelität“ sind hier nur ein Makrogefühl. Auf der Mikroebene handelt es sich tatsächlich nur um die Rotationsausführung von Prozessen/Threads, aber die Umschaltzeit ist sehr kurz. „Paralleles“ Gefühl.
Multithreading vs. Multiprozess
Das Betriebssystem weist jedem Prozess unterschiedliche Speicherblöcke zu und mehrere Threads teilen sich den Speicherblock des Prozesses. Der direkteste Unterschied besteht darin, dass die Kosten für die Erstellung eines Threads viel geringer sind als die Kosten für die Erstellung eines Prozesses.
Gleichzeitig ist die Kommunikation zwischen Prozessen aufgrund unterschiedlicher Speicherblöcke relativ schwierig. Es ist notwendig, Pipe/Named Pipe, Signal, Nachrichtenwarteschlange, gemeinsam genutzten Speicher, Socket und andere Mittel zu verwenden, und die Kommunikation zwischen Threads ist einfach und schnell, was die gemeinsame Nutzung globaler Variablen im Prozess bedeutet.
Das Betriebssystem ist jedoch für die Prozessplanung verantwortlich, und die Thread-Planung muss von uns selbst berücksichtigt werden, um Deadlock, Hunger, Livelock, Ressourcenerschöpfung usw. zu vermeiden, was zu einer gewissen Komplexität führt. Da der Speicher außerdem von Threads gemeinsam genutzt wird, müssen wir auch Aspekte der Thread-Sicherheit berücksichtigen.
Thread-Sicherheit
Threads teilen im Prozess globale Variablen. Wenn also andere Threads die gemeinsam genutzten Variablen ändern, kann dies Auswirkungen auf diesen Thread haben. Die sogenannte Thread-Sicherheitsbeschränkung bedeutet, dass eine Funktion, wenn sie wiederholt von mehreren gleichzeitigen Threads aufgerufen wird, immer korrekte Ergebnisse liefern muss. Um die Thread-Sicherheit zu gewährleisten, besteht die Hauptmethode darin, durch Sperren den korrekten Zugriff auf gemeinsam genutzte Variablen sicherzustellen.
Eine strengere Einschränkung als die Thread-Sicherheit ist „Wiedereintritt“, das heißt, eine Funktion wird während der Ausführung in einem Thread angehalten, dann in einem anderen Thread aufgerufen und kehrt dann zum ursprünglichen Thread zurück. Während des gesamten Prozesses ist eine ordnungsgemäße Ausführung gewährleistet. Die Wiedereintrittsfähigkeit wird normalerweise dadurch gewährleistet, dass lokale Kopien globaler Variablen erstellt werden.
Thread-Lebenszyklus
Der sogenannte xx-Lebenszyklus ist eigentlich ein Zustandsdiagramm, das die Erstellung und Zerstörung eines Objekts umfasst. Der Lebenszyklus des Threads ist in der folgenden Abbildung dargestellt:
Die Beschreibung jedes Status lautet wie folgt:
Neu. Nachdem der neu erstellte Thread initialisiert wurde, wechselt er in den ausführbaren Zustand.
Runnable ist fertig. Warten auf Thread-Planung. Geben Sie nach der Planung den Betriebsstatus ein.
Laufen.
Blockiert. Unterbrechen Sie den Vorgang, entsperren Sie ihn und wechseln Sie in den Status „Ausführbar“, um auf die erneute Planung zu warten.
Tot. Die Thread-Methode kehrt nach der Ausführung zurück oder wird abnormal beendet.
Es kann drei Situationen geben, in denen Sie „Blocked from Running“ eingeben:
Synchronisierung: Wenn ein Thread eine Synchronisierungssperre erhält, die Ressource jedoch bereits von anderen Threads gesperrt ist, wechselt sie in den gesperrten Zustand bis die Ressource verfügbar ist (Erwerb Die Reihenfolge wird durch die Sperrwarteschlange gesteuert)
Ruhezustand: Nachdem der Thread die Methode „sleep()“ oder „join()“ ausgeführt hat, wechselt der Thread in den Ruhezustand. Der Unterschied besteht darin, dass Sleep eine bestimmte Zeit lang wartet, während Join darauf wartet, dass die Ausführung des untergeordneten Threads abgeschlossen ist. Natürlich kann bei Join auch eine „Timeout-Zeitspanne“ angegeben werden. Wenn zwei Threads a und b in a b.join () aufrufen, entspricht dies semantisch gesehen dem Zusammenführen (Join) zu einem Thread. Die häufigste Situation besteht darin, alle untergeordneten Threads im Hauptthread zusammenzuführen.
Warten: Nach der Ausführung der Methode wait() im Thread wechselt der Thread in den Wartezustand und wartet auf Benachrichtigungen von anderen Threads.
Arten von Threads
Hauptthread: Wenn ein Programm gestartet wird, wird vom Betriebssystem (OS) ein Prozess erstellt und gleichzeitig wird sofort ein Thread ausgeführt. Dieser Thread wird normalerweise aufgerufen ein Programm-Hauptthread. Jeder Prozess verfügt über mindestens einen Hauptthread, und der Hauptthread wird normalerweise zuletzt heruntergefahren.
Unterthreads: Andere im Programm erstellte Threads sind Unterthreads dieses Hauptthreads relativ zum Hauptthread.
Daemon-Thread: Daemon-Thread, eine Identifikation eines Threads. Daemon-Threads stellen Dienste für andere Threads bereit, beispielsweise für den Garbage-Collection-Thread der JVM. Wenn alle Daemon-Threads übrig sind, wird der Prozess beendet.
Vordergrund-Thread: Andere Threads relativ zum Daemon-Thread werden Vordergrund-Threads genannt.
Python-Unterstützung für Multithreading
Virtuelle Maschinenebene
Die virtuelle Python-Maschine verwendet GIL (Global Interpreter Lock, globale Interpretersperre), um Threads gegenseitig vom Zugriff auf gemeinsam genutzte Ressourcen auszuschließen, und ist vorübergehend nicht in der Lage, die Vorteile von Multiprozessoren zu nutzen.
Sprachebene
Auf Sprachebene bietet Python gute Unterstützung für Multithreading-bezogene Module in Python, darunter: Thread, Threading und Queue. Es kann problemlos Funktionen wie Thread-Erstellung, Mutex-Sperren, Semaphoren und Synchronisierung unterstützen.
Thread: Das zugrunde liegende Unterstützungsmodul für Multithreading, im Allgemeinen nicht empfohlen.
Threading: Thread ist gekapselt, einige Thread-Operationen werden objektiviert und die folgenden Klassen werden bereitgestellt:
Thread-Thread-Klasse
Timer ähnelt Thread, muss es aber sein Warten Sie eine Weile. Zeit, mit der Ausführung zu beginnen. Ermöglicht einem einzelnen Thread, eine bereits erworbene Sperre erneut zu erhalten
Bedingungsvariable Bedingung, die es einem Thread ermöglicht, anzuhalten und darauf zu warten, dass andere Threads eine bestimmte „Bedingung“ erfüllen
Ereignis Allgemeine Bedingungsvariable. Mehrere Threads können auf das Eintreten eines Ereignisses warten. Alle Threads werden aktiviert.
Semaphore bietet eine „Warteraum“-ähnliche Struktur für Threads, die auf Sperren warten.
BoundedSemaphore und Ähnliches Semaphor, darf jedoch den Anfangswert nicht überschreiten
Warteschlange: Implementiert eine Warteschlange mit mehreren Produzenten (Produzenten) und mehreren Verbrauchern (Verbraucher), unterstützt Sperrprimitive und kann einen guten Service für die Synchronisierung mehrerer Threads bieten . Bereitgestellte Klassen:
Warteschlangenwarteschlange
LifoQueue Last-in-First-out (LIFO)-Warteschlange
PriorityQueue-Prioritätswarteschlange
wobei die Thread-Klasse Ihre ist Hauptthread-Klasse, die Prozessinstanzen erstellen kann. Zu den von dieser Klasse bereitgestellten Funktionen gehören:
getName(self) gibt den Namen des Threads zurück
isAlive(self) Boolesches Flag, das angibt, ob dieser Thread noch läuft
isDaemon (self) Gibt das Daemon-Flag des Threads zurück
join(self, timeout=None) Das Programm bleibt hängen, bis der Thread endet. Wenn ein Timeout angegeben ist, wird es für bis zu Timeout-Sekunden blockiert
run(self) Definition Thread-Funktion function
setDaemon(self, daemonic) Setzt das Daemon-Flag des Threads auf daemonic
setName(self, name) Setzt den Namen des Threads
start(self) Thread-Ausführung starten
Unterstützung von Drittanbietern
Wenn Ihnen die Leistung besonders am Herzen liegt, können Sie auch einige „Micro-Threading“-Implementierungen in Betracht ziehen:
Stackless Python: eine erweiterte Version von Python, bietet Unterstützung für Microthreads. Microthreads sind leichtgewichtige Threads, die mehr Zeit zum Wechseln zwischen mehreren Threads benötigen und weniger Ressourcen beanspruchen.
Greenlet: Ein Nebenprodukt von Stackless, das Mikro-Threads „Tasklets“ nennt. Tasklets laufen in Pseudo-Parallelität und nutzen Kanäle für den synchronen Datenaustausch. Und „Greenlet“ ist ein primitiveres Konzept von Mikro-Threads ohne Planung. Sie können selbst einen Mikro-Thread-Scheduler erstellen oder Greenlets verwenden, um einen erweiterten Kontrollfluss zu implementieren.
Im nächsten Abschnitt beginnen Sie mit dem Erstellen und Starten von Threads in Python.