Mit dem Aufkommen von Python 3 wird viel über „Asynchronismus“ und „Parallelität“ geredet. Wir können davon ausgehen, dass Python diese Funktionen / Konzepte kürzlich eingeführt hat. Aber das ist nicht so. Wir haben diese Operationen viele Male benutzt. Darüber hinaus denken Anfänger möglicherweise, dass Asyncio die einzige oder beste Möglichkeit ist, asynchrone / parallele Operationen neu zu erstellen und zu verwenden. In diesem Artikel werden verschiedene Möglichkeiten zur Erzielung von Parallelität sowie deren Vor- und Nachteile untersucht.
Begriffsdefinition:
Bevor wir uns mit den technischen Aspekten befassen, ist es wichtig, ein grundlegendes Verständnis der in diesem Zusammenhang häufig verwendeten Begriffe zu haben.
Synchron und asynchron:Bei
synchronen Vorgängen werden Aufgaben nacheinander ausgeführt. In
asynchronen Aufgaben können unabhängig voneinander gestartet und ausgeführt werden. Eine asynchrone Aufgabe kann gestartet und fortgesetzt werden, während die Ausführung zu einer neuen Aufgabe wechselt. Asynchrone Aufgaben blockieren keine Vorgänge (erzwingen Sie nicht das Warten auf den Abschluss der Aufgabe) und werden normalerweise im Hintergrund ausgeführt.
Sie sollten sich beispielsweise an ein Reisebüro wenden, um Ihren nächsten Urlaub zu planen. Sie müssen einen Brief an Ihren Vorgesetzten senden, bevor Sie fliegen. Im synchronen Modus rufen Sie zuerst das Reisebüro an. Wenn Sie aufgefordert werden zu warten, warten Sie, bis sie Ihnen antworten. Dann werden Sie beginnen, einen Brief an den Führer zu schreiben. So erledigen Sie die Aufgaben nacheinander.
[synchrone Ausführung, ca. Übersetzer] Aber wenn Sie schlau sind, haben sie Sie gebeten zu warten
. Übersetzer] Sie beginnen mit dem Schreiben von E-Mails. Wenn Sie erneut sprechen, unterbrechen Sie das Schreiben, sprechen und fügen dann den Brief hinzu. Sie können auch einen Freund bitten, die Agentur anzurufen und selbst einen Brief zu schreiben. Dies ist Asynchronität, Aufgaben blockieren sich nicht gegenseitig.
Wettbewerbsfähigkeit und Parallelität:Wettbewerbsfähigkeit bedeutet, dass zwei Aufgaben
gemeinsam ausgeführt werden . In unserem vorherigen Beispiel haben wir, als wir das asynchrone Beispiel betrachteten, schrittweise einen Brief geschrieben und dann ein Gespräch mit einer Tour geführt. Agentur. Das ist
Wettbewerbsfähigkeit .
Als wir darum baten, einen Freund anzurufen, und selbst einen Brief schrieben, wurden die Aufgaben
parallel ausgeführt .
Parallelität ist im Wesentlichen eine Form des Wettbewerbs. Die Parallelität ist jedoch hardwareabhängig. Wenn die CPU beispielsweise nur einen Kern hat, können zwei Aufgaben nicht parallel ausgeführt werden. Sie teilen einfach die Prozessorzeit untereinander. Dann ist dies Wettbewerb, aber keine Parallelität. Aber wenn wir mehrere Kerne haben
[als Freund im vorherigen Beispiel, das der zweite Kern ist, ca. Übersetzer] können wir mehrere Operationen (abhängig von der Anzahl der Kerne) gleichzeitig ausführen.
Zusammenfassend:
- Synchronisation: Blockiert Operationen (Blockieren)
- Asynchronität: Blockiert keine Operationen (nicht blockierend)
- Wettbewerbsfähigkeit: gemeinsamer Fortschritt (gemeinsam)
- Parallelität: paralleler Fortschritt (parallel)
Parallelität impliziert Wettbewerb. Wettbewerb bedeutet jedoch nicht immer Parallelität.
Themen und Prozesse
Python unterstützt Threads seit sehr langer Zeit. Mit Threads können Sie Vorgänge wettbewerbsfähig ausführen. Es gibt jedoch ein Problem mit der
globalen Interpreter-Sperre (GIL), aufgrund dessen Threads keine echte Parallelität bieten konnten. Mit dem Aufkommen der Mehrfachverarbeitung können Sie jedoch mit Python mehrere Kerne verwenden.
ThemenBetrachten Sie ein kleines Beispiel. Im folgenden Code wird die
Worker- Funktion auf mehreren Threads asynchron und gleichzeitig ausgeführt.
import threading import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".format(number, sleep)) for i in range(5): t = threading.Thread(target=worker, args=(i,)) t.start() print("All Threads are queued, let's see when they finish!")
Und hier ist eine Beispielausgabe:
$ python thread_test.py All Threads are queued, let's see when they finish! I am Worker 1, I slept for 1 seconds I am Worker 3, I slept for 4 seconds I am Worker 4, I slept for 5 seconds I am Worker 2, I slept for 7 seconds I am Worker 0, I slept for 9 seconds
Daher haben wir 5 Threads für die Zusammenarbeit gestartet. Nach dem Start (d. H. Nach dem Ausführen der Worker-Funktion)
wartet der Vorgang
nicht auf den Abschluss der Threads, bevor mit der nächsten Druckanweisung fortgefahren wird. Dies ist eine asynchrone Operation.
In unserem Beispiel haben wir die Funktion an den Thread-Konstruktor übergeben. Wenn wir wollten, könnten wir eine Unterklasse mit einer Methode (OOP-Stil) implementieren.
Weiterführende Literatur:Um mehr über Streams zu erfahren, verwenden Sie den folgenden Link:
Global Interpreter Lock (GIL)GIL wurde eingeführt, um die Speicherbehandlung von CPython zu vereinfachen und die beste Integration mit C (z. B. mit Erweiterungen) zu ermöglichen. GIL ist ein Sperrmechanismus, wenn der Python-Interpreter jeweils nur einen Thread ausführt. Das heißt, Es kann jeweils nur ein Thread in Python-Bytecode ausgeführt werden. GIL stellt sicher, dass nicht mehrere Threads
parallel ausgeführt werden .
GIL Schnelle Details:
- Es kann jeweils ein Thread ausgeführt werden.
- Der Python-Interpreter wechselt zwischen Threads, um Wettbewerbsfähigkeit zu erzielen.
- GIL gilt für CPython (Standardimplementierung). Aber wie zum Beispiel Jython und IronPython haben keine GIL.
- GIL macht Single-Threaded-Programme schnell.
- GIL stört normalerweise nicht die E / A.
- GIL macht es einfach, thread-sichere Bibliotheken in C zu integrieren. Dank GIL haben wir viele leistungsstarke Erweiterungen / Module in C geschrieben.
- Bei CPU-abhängigen Aufgaben überprüft der Interpreter alle N Ticks und wechselt die Threads. Daher blockiert ein Thread die anderen nicht.
Viele sehen GIL als Schwäche. Ich betrachte dies als Segen, da Bibliotheken wie NumPy, SciPy geschaffen wurden, die eine besondere, einzigartige Position in der wissenschaftlichen Gemeinschaft einnehmen.
Weiterführende Literatur:Mit diesen Ressourcen können Sie sich mit der GIL befassen:
ProzesseUm Parallelität in Python zu erreichen, wurde ein
Multiprozessor- Modul hinzugefügt, das eine API bereitstellt und sehr ähnlich aussieht, wenn Sie zuvor
Threading verwendet haben.
Lassen Sie uns einfach das vorherige Beispiel ändern. Jetzt verwendet die geänderte Version den
Prozess anstelle des
Streams .
import multiprocessing import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".format(number, sleep)) for i in range(5): t = multiprocessing.Process(target=worker, args=(i,)) t.start() print("All Processes are queued, let's see when they finish!")
Was hat sich geändert? Ich habe gerade das
Multiprocessing- Modul anstelle von
Threading importiert. Und dann habe ich anstelle eines Threads einen Prozess verwendet. Das ist alles! Anstelle vieler Threads verwenden wir jetzt Prozesse, die auf verschiedenen CPU-Kernen ausgeführt werden (es sei denn, Ihr Prozessor verfügt natürlich über mehrere Kerne).
Mit der Pool-Klasse können wir auch die Ausführung einer Funktion auf mehrere Prozesse für unterschiedliche Eingabewerte verteilen. Ein Beispiel aus den offiziellen Dokumenten:
from multiprocessing import Pool def f(x): return x*x if __name__ == '__main__': p = Pool(5) print(p.map(f, [1, 2, 3]))
Anstatt die Werteliste zu durchlaufen und die Funktion f einzeln aufzurufen, führen wir die Funktion tatsächlich in verschiedenen Prozessen aus. Ein Prozess macht f (1), der andere f (2) und der andere f (3). Schließlich werden die Ergebnisse wieder zu einer Liste zusammengefasst. Dies ermöglicht es uns, schwere Berechnungen in kleinere Teile zu zerlegen und diese für eine schnellere Berechnung parallel auszuführen.
Weiterführende Literatur:Concurrent.futures-ModulDas concurrent.futures-Modul ist groß und erleichtert das Schreiben von asynchronem Code. Meine Favoriten sind
ThreadPoolExecutor und
ProcessPoolExecutor . Diese Künstler unterstützen einen Pool von Threads oder Prozessen. Wir senden unsere Aufgaben an den Pool und er führt die Aufgaben in einem zugänglichen Thread / Prozess aus. Es wird ein
Future- Objekt zurückgegeben, mit dem das Ergebnis nach Abschluss der Aufgabe abgefragt und abgerufen werden kann.
Und hier ist ein Beispiel für ThreadPoolExecutor:
from concurrent.futures import ThreadPoolExecutor from time import sleep def return_after_5_secs(message): sleep(5) return message pool = ThreadPoolExecutor(3) future = pool.submit(return_after_5_secs, ("hello")) print(future.done()) sleep(5) print(future.done()) print(future.result())
Ich habe einen Artikel über concurrent.futures
masnun.com/2016/03/29/python-a-quick-introduction-to-the-concurrent-futures-module.html . Dies kann für eine eingehendere Untersuchung dieses Moduls hilfreich sein.
Weiterführende Literatur:Asyncio - was, wie und warum?
Sie haben wahrscheinlich eine Frage, die viele Leute in der Python-Community haben - was bringt Asyncio Neues? Warum gab es eine andere Möglichkeit, asynchrone E / A zu verwenden? Hatten wir nicht schon Threads und Prozesse? Mal sehen!
Warum brauchen wir Asyncio?Prozesse sind sehr teuer
[in Bezug auf den Ressourcenverbrauch ca. Übersetzer] zu erstellen. Daher werden für E / A-Operationen hauptsächlich Threads ausgewählt. Wir wissen, dass E / A von externen Faktoren abhängt - langsame Laufwerke oder unangenehme Netzwerkverzögerungen machen E / A oft unvorhersehbar. Angenommen, wir verwenden Threads für E / A. 3 Threads führen verschiedene E / A-Aufgaben aus. Der Dolmetscher müsste zwischen den Wettbewerbsströmen wechseln und jedem von ihnen nacheinander etwas Zeit geben. Nennen Sie die Flüsse T1, T2 und T3. Drei Threads haben ihre E / A-Operation gestartet. T3 schließt es zuerst ab. T2 und T1 warten noch auf I / O. Der Python-Interpreter wechselt zu T1, wartet aber noch. Nun, der Interpreter wechselt zu T2, und der Interpreter wartet noch und wechselt dann zu T3, das bereit ist und den Code ausführt. Sehen Sie das als Problem?
T3 war bereit, aber der Dolmetscher wechselte zuerst zwischen T2 und T1 - dies verursachte Schaltkosten, die wir hätten vermeiden können, wenn der Dolmetscher zuerst auf T3 gewechselt hätte, oder?
Was ist Asynio?Asyncio bietet uns eine Event-Schleife zusammen mit anderen coolen Sachen. Die Ereignisschleife überwacht E / A-Ereignisse und wechselt Aufgaben, die bereit sind und auf E / A-Vorgänge warten.
[Die Ereignisschleife ist ein Softwarekonstrukt, das auf die Ankunft wartet und Ereignisse oder Nachrichten im Programm sendet. Übersetzer] .
Die Idee ist sehr einfach. Es gibt eine Ereignisschleife. Und wir haben Funktionen, die asynchrone E / A ausführen. Wir übertragen unsere Funktionen in die Ereignisschleife und bitten ihn, sie für uns auszuführen. Die Ereignisschleife gibt uns ein Future-Objekt zurück, wie ein Versprechen, dass wir in Zukunft etwas bekommen werden. Wir halten an einem Versprechen fest, prüfen von Zeit zu Zeit, ob es wichtig ist (wir können wirklich nicht warten), und schließlich verwenden wir es, wenn der Wert empfangen wird, in einigen anderen Operationen
[d.h. Wir schickten eine Anfrage, bekamen sofort ein Ticket und mussten warten, bis das Ergebnis eintrifft. Wir überprüfen das Ergebnis regelmäßig und sobald es eingegangen ist, nehmen wir ein Ticket und erhalten einen Wert darauf, ca. Übersetzer] .
Asyncio verwendet Generatoren und Coroutinen, um Aufgaben zu stoppen und fortzusetzen. Sie können die Details hier lesen:
Wie benutzt man Asyncio?Bevor wir anfangen, schauen wir uns ein Beispiel an:
import asyncio import datetime import random async def my_sleep_func(): await asyncio.sleep(random.randint(0, 5)) async def display_date(num, loop): end_time = loop.time() + 50.0 while True: print("Loop: {} Time: {}".format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break await my_sleep_func() loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop)) asyncio.ensure_future(display_date(2, loop)) loop.run_forever()
Beachten Sie, dass die Syntax async / await nur für Python 3.5 und höher gilt. Lassen Sie uns den Code durchgehen:
- Wir haben eine asynchrone display_date-Funktion, die eine Zahl (als Bezeichner) und eine Ereignisschleife als Parameter verwendet.
- Die Funktion hat eine Endlosschleife, die nach 50 Sekunden unterbrochen wird. Aber während dieser Zeit druckt sie wiederholt Zeit und pausiert. Die Wartefunktion kann warten, bis andere asynchrone Funktionen abgeschlossen sind (Coroutine).
- Wir übergeben die Funktion an die Ereignisschleife (unter Verwendung der Methode verify_future).
- Wir beginnen einen Zyklus von Ereignissen.
Immer wenn "wait" aufgerufen wird, erkennt asyncio, dass die Funktion wahrscheinlich einige Zeit dauern wird. Daher wird die Ausführung angehalten, die Überwachung aller damit verbundenen E / A-Ereignisse gestartet und Sie können Aufgaben ausführen. Wenn asyncio feststellt, dass die angehaltene Funktions-E / A bereit ist, wird die Funktion fortgesetzt.
Die richtige Wahl treffen.
Wir haben gerade die beliebtesten Formen der Wettbewerbsfähigkeit durchlaufen. Aber die Frage bleibt - was sollte gewählt werden? Dies hängt von den Anwendungsfällen ab. Aus meiner Erfahrung folge ich eher diesem Pseudocode:
if io_bound: if io_very_slow: print("Use Asyncio") else: print("Use Threads") else: print("Multi Processing")
- CPU Bound => Multi Processing
- E / A-gebunden, schnelle E / A, begrenzte Anzahl von Verbindungen => Multi-Threading
- E / A gebunden, langsame E / A, viele Verbindungen => Asyncio
[Anmerkung Übersetzer]