Multithreading ermöglicht es einem Prozess, mehrere Threads gleichzeitig auszuführen, wobei Threads denselben Speicher und dieselben Ressourcen teilen (siehe Diagramme 2 und 4).
Allerdings schränkt Pythons International Interpreter Lock (GIL) die Effektivität von Multithreading für CPU-gebundene Aufgaben ein.
Pythons International Interpreter Lock (GIL)
Die GIL ist eine Sperre, die es jeweils nur einem Thread ermöglicht, die Kontrolle über den Python-Interpreter zu behalten, was bedeutet, dass nur ein Thread gleichzeitig Python-Bytecode ausführen kann.
Die GIL wurde eingeführt, um die Speicherverwaltung in Python zu vereinfachen, da viele interne Vorgänge, wie z. B. die Objekterstellung, standardmäßig nicht threadsicher sind. Ohne eine GIL erfordern mehrere Threads, die versuchen, auf die gemeinsam genutzten Ressourcen zuzugreifen, komplexe Sperren oder Synchronisierungsmechanismen, um Race Circumstances und Datenbeschädigungen zu verhindern.
Wann ist GIL ein Engpass?
- Für Single-Threaded-Programme ist die GIL irrelevant, da der Thread exklusiven Zugriff auf den Python-Interpreter hat.
- Für Multithread-E/A-gebundene Programme ist die GIL weniger problematisch, da Threads die GIL freigeben, wenn sie auf E/A-Vorgänge warten.
- Bei Multithread-CPU-gebundenen Vorgängen wird die GIL zu einem erheblichen Engpass. Mehrere Threads, die um die GIL konkurrieren, müssen abwechselnd Python-Bytecode ausführen.
Ein interessanter erwähnenswerter Fall ist die Verwendung von time.sleep
was Python effektiv als E/A-Vorgang behandelt. Der time.sleep
Die Funktion ist nicht an die CPU gebunden, da sie keine aktive Berechnung oder die Ausführung von Python-Bytecode während der Ruhephase erfordert. Stattdessen wird die Verantwortung für die Verfolgung der verstrichenen Zeit an das Betriebssystem delegiert. Während dieser Zeit gibt der Thread die GIL frei, sodass andere Threads ausgeführt und den Interpreter nutzen können.
Multiprocessing ermöglicht es einem System, mehrere Prozesse parallel auszuführen, jeder mit eigenem Speicher, eigener GIL und eigenen Ressourcen. Innerhalb jedes Prozesses kann es einen oder mehrere Threads geben (siehe Diagramme 3 und 4).
Multiprocessing umgeht die Einschränkungen der GIL. Dadurch eignet es sich für CPU-gebundene Aufgaben, die einen hohen Rechenaufwand erfordern.
Allerdings ist Multiprocessing aufgrund des separaten Speicher- und Prozess-Overheads ressourcenintensiver.
Im Gegensatz zu Threads oder Prozessen verwendet Asyncio einen einzelnen Thread, um mehrere Aufgaben abzuwickeln.
Beim Schreiben von asynchronem Code mit dem asyncio
Bibliothek, Sie werden die verwenden async/await
Schlüsselwörter zum Verwalten von Aufgaben.
Schlüsselkonzepte
- Coroutinen: Dies sind Funktionen, die mit definiert sind
async def
. Sie sind der Kern von Asyncio und stellen Aufgaben dar, die angehalten und später wieder aufgenommen werden können. - Ereignisschleife: Es verwaltet die Ausführung von Aufgaben.
- Aufgaben: Wrapper um Coroutinen. Wenn Sie möchten, dass eine Coroutine tatsächlich ausgeführt wird, verwandeln Sie sie in eine Aufgabe – z. verwenden
asyncio.create_task()
await
: Unterbricht die Ausführung einer Coroutine und gibt die Kontrolle wieder an die Ereignisschleife zurück.
Wie es funktioniert
Asyncio führt eine Ereignisschleife aus, die Aufgaben plant. Aufgaben „pausieren“ sich freiwillig, wenn sie auf etwas warten, beispielsweise auf eine Netzwerkantwort oder das Lesen einer Datei. Während die Aufgabe angehalten ist, wechselt die Ereignisschleife zu einer anderen Aufgabe, sodass keine Zeit mit Warten verschwendet wird.
Dies macht Asyncio best für Szenarien mit viele kleine Aufgaben, die viel Zeit mit Warten in Anspruch nehmenB. die Bearbeitung tausender Webanfragen oder die Verwaltung von Datenbankabfragen. Da alles in einem einzigen Thread läuft, vermeidet Asyncio den Overhead und die Komplexität des Thread-Wechsels.
Der Hauptunterschied zwischen Asyncio und Multithreading liegt in der Artwork und Weise, wie sie mit wartenden Aufgaben umgehen.
- Multithreading ist darauf angewiesen, dass das Betriebssystem zwischen Threads wechselt, wenn ein Thread wartet (Präventiver Kontextwechsel).
Wenn ein Thread wartet, wechselt das Betriebssystem automatisch zu einem anderen Thread. - Asyncio verwendet einen einzelnen Thread und ist darauf angewiesen, dass Aufgaben „kooperieren“, indem sie pausieren, wenn sie warten müssen (kooperatives Multitasking).
2 Möglichkeiten, asynchronen Code zu schreiben:
methodology 1: await coroutine
Wenn Sie direkt await
eine Coroutine, die Ausführung der Die aktuelle Coroutine pausiert am await
Anweisung, bis die erwartete Coroutine beendet ist. Aufgaben werden ausgeführt Der Reihe nach innerhalb der aktuellen Coroutine.
Verwenden Sie diesen Ansatz, wenn Sie das Ergebnis der Coroutine benötigen sofort um mit den nächsten Schritten fortzufahren.
Auch wenn das wie synchroner Code klingt, ist es das nicht. Im synchronen Code würde das gesamte Programm während einer Pause blockieren.
Bei Asyncio pausiert nur die aktuelle Coroutine, während der Relaxation des Programms weiterlaufen kann. Dadurch ist Asyncio auf Programmebene nicht blockierend.
Beispiel:
Die Ereignisschleife hält die aktuelle Coroutine an, bis fetch_data
ist abgeschlossen.
async def fetch_data():
print("Fetching knowledge...")
await asyncio.sleep(1) # Simulate a community name
print("Information fetched")
return "knowledge"async def principal():
consequence = await fetch_data() # Present coroutine pauses right here
print(f"Consequence: {consequence}")
asyncio.run(principal())
methodology 2: asyncio.create_task(coroutine)
Die Coroutine ist geplant laufen gleichzeitig im Hintergrund. Im Gegensatz zu await
wird die aktuelle Coroutine sofort weiter ausgeführt, ohne auf den Abschluss der geplanten Aufgabe zu warten.
Die geplante Coroutine beginnt mit der Ausführung, sobald die Ereignisschleife eine Gelegenheit findetohne auf ein explizites warten zu müssen await
.
Es werden keine neuen Threads erstellt. Stattdessen läuft die Coroutine im selben Thread wie die Ereignisschleife, die verwaltet, wann jede Aufgabe Ausführungszeit erhält.
Dieser Ansatz ermöglicht Parallelität innerhalb des Programms, sodass mehrere Aufgaben ihre Ausführung effizient überlappen können. Das werden Sie später noch tun müssen await
die Aufgabe, das Ergebnis zu erhalten und sicherzustellen, dass es erledigt wird.
Verwenden Sie diesen Ansatz, wenn Sie Aufgaben gleichzeitig ausführen möchten und die Ergebnisse nicht sofort benötigen.
Beispiel:
Wenn die Linie asyncio.create_task()
erreicht ist, die Coroutine fetch_data()
Es ist geplant, mit der Ausführung zu beginnen sofort, wenn die Ereignisschleife verfügbar ist. Dies kann sogar passieren vor Sie ausdrücklich await
die Aufgabe. Im Gegensatz dazu im ersten await
Methode beginnt die Coroutine erst mit der Ausführung, wenn die await
Aussage erreicht ist.
Insgesamt wird das Programm dadurch effizienter, da die Ausführung mehrerer Aufgaben überlappt.
async def fetch_data():
# Simulate a community name
await asyncio.sleep(1)
return "knowledge"async def principal():
# Schedule fetch_data
activity = asyncio.create_task(fetch_data())
# Simulate doing different work
await asyncio.sleep(5)
# Now, await activity to get the consequence
consequence = await activity
print(consequence)
asyncio.run(principal())
Weitere wichtige Punkte
- Sie können synchronen und asynchronen Code mischen.
Da synchroner Code blockiert, kann er mit in einen separaten Thread ausgelagert werdenasyncio.to_thread()
. Dadurch wird Ihr Programm effektiv multithreadfähig.
Im folgenden Beispiel wird die Asyncio-Ereignisschleife im Hauptthread ausgeführt, während ein separater Hintergrundthread zur Ausführung verwendet wirdsync_task
.
import asyncio
import timedef sync_task():
time.sleep(2)
return "Accomplished"
async def principal():
consequence = await asyncio.to_thread(sync_task)
print(consequence)
asyncio.run(principal())
- Sie sollten CPU-gebundene Aufgaben, die rechenintensiv sind, auf einen separaten Prozess auslagern.
Dieser Ablauf ist eine gute Möglichkeit zu entscheiden, wann was verwendet werden soll.
- Mehrfachverarbeitung
– Am besten für CPU-gebundene Aufgaben geeignet, die rechenintensiv sind.
– Wenn Sie die GIL umgehen müssen – Jeder Prozess verfügt über einen eigenen Python-Interpreter, der echte Parallelität ermöglicht. - Multithreading
– Am besten für schnelle I/O-gebundene Aufgaben geeignet, da die Häufigkeit des Kontextwechsels reduziert wird und der Python-Interpreter länger bei einem einzelnen Thread bleibt
– Aufgrund von GIL nicht best für CPU-gebundene Aufgaben. - Asyncio
– Supreme für langsame I/O-gebundene Aufgaben wie lange Netzwerkanfragen oder Datenbankabfragen, da es Wartezeiten effizient verarbeitet und dadurch skalierbar ist.
– Nicht für CPU-gebundene Aufgaben geeignet, ohne dass die Arbeit auf andere Prozesse verlagert wird.
Das ist es, Leute. Dieses Thema deckt noch viel mehr ab, aber ich hoffe, ich habe Ihnen die verschiedenen Konzepte und den Einsatz der einzelnen Methoden vorgestellt.
Danke fürs Lesen! Ich schreibe regelmäßig über Python, Softwareentwicklung und die Projekte, die ich erstelle, additionally folgen Sie mir, um nichts zu verpassen. Wir sehen uns im nächsten Artikel 🙂