Erste Schritte mit der asynchronen Python-Programmierung

1. Was ist asynchron?

Wenn wir vom asynchronen Modell sprechen, müssen wir das allgemeine synchrone Modell erwähnen, bei dem es sich um relative Konzepte handelt.

Das Synchronisationsmodell bedeutet, dass das Programm nacheinander ausgeführt werden muss. Wenn das Programm einen Vorgang ausführt, der auf externe Ressourcen warten muss (Senden und Empfangen von Netzwerkdaten, Lesen und Schreiben von Dateien), fällt es in einen Zustand und wird nur fortgesetzt Wird ausgeführt, nachdem die externen Ressourcen vorhanden sind 阻塞. Im Gegensatz dazu weist das asynchrone Modell 非阻塞die Eigenschaft auf, dass das Programm weiterhin anderen Code ausführt, während es auf externe Ressourcen wartet.

In Version 3.4 führte Python die Unterstützung für asynchrone Programmierung ein. Unter demselben Thread werden mehrere Coroutinen über die Ereignisschleife geplant, um Code-Slices zu wechseln, wodurch die negativen Auswirkungen des Blockierens auf die Programmleistung und der durch das Umschalten verursachte zusätzliche Overhead des Threads beseitigt werden können .

In einem Thread können Benutzer asynchrone Aufgaben (z. B. Coroutinen) erstellen und diese zur einheitlichen Planung und Verwaltung an die Ereignisschleife übergeben ( event loop). Eine Coroutine ( Coroutines) ist eine Codeausführungseinheit, die eine Ebene kleiner als ein Thread ist. Gleichzeitig läuft nur eine Coroutine in der Ereignisschleife. Wenn die Coroutine auf externe Ressourcen wartet, wird der Thread nicht blockiert , sondern das Ausführungsrecht übergeben , sodass die Ereignisschleife weiterhin andere Coroutinen ausführt . Das Obige ist der grundlegende Inhalt des asynchronen Python-Modells. Die spezifische Operation wird später in Kapitel 3 ausführlich beschrieben.

Zweitens, warum asynchron?

Bevor Sie lernen, wie man in Python asynchron programmiert, denken Sie bitte zweimal darüber nach, warum Sie in Ihrem Code asynchron wählen sollten.

Erstens der Leistungsvorteil . Für den Python-Interpreter, den wir täglich verwenden, scheint GIL ein unvermeidbares Thema zu sein (GIL wird im nächsten Blog „ Python-Multithreading“ ausführlich beschrieben ). Aufgrund der Einschränkung der GIL führt die CPU tatsächlich nur einen Thread gleichzeitig aus, und die gleichzeitige Leistung ist stark eingeschränkt. Wenn ein Thread in einen blockierten Zustand gerät, wird die GIL-Sperre aufgehoben und das Ausführungsrecht an andere Threads übergeben, um die Ausführung fortzusetzen. Auf den ersten Blick scheint es dem oben erwähnten Prinzip der Coroutine-Ausführung sehr ähnlich zu sein.

Tatsächlich ist der Prozess sehr ähnlich: Wenn die Coroutine während der Ausführung auf E/A-Vorgänge stößt, wechselt sie zu anderen Coroutinen, um die Ausführung fortzusetzen. In einem Multithread-Szenario ist der durch Blockieren und Thread-Wechsel verursachte Overhead jedoch viel höher als die Kosten für die Coroutine-Planung, und die von Coroutinen belegten Speicherressourcen sind viel kleiner als die von Threads . Daher kann die Übergabe von Codeabschnitten mithilfe von Coroutinen in einem Thread zu sehr geringen Kosten durchgeführt werden. Diese Lücke wird, wenn sie in bestimmten Anwendungsszenarien platziert wird, erhebliche Leistungsverbesserungen mit sich bringen.

Darüber hinaus Programmiererfahrung . Entwickler müssen eine Reihe von Problemen wie Sperrressourcenkonkurrenz und -freigabe, Deadlock-Probleme, Thread-Synchronisierung usw. nicht mehr berücksichtigen, sodass die asynchrone Programmierung ein relativ einfacheres und intuitiveres Programmiermodell ist.

3. Wie kann man asynchron sein?

Dieser Abschnitt asynciokonzentriert sich auf die zugehörigen Vorgänge der asynchronen E/A-Bibliothek in der Python-Standardbibliothek. Es gibt andere Bibliotheken, die auch asynchrone Programmierung unterstützen, wie zum Beispiel Twisted, Tornado usw. Interessierte Freunde können sich anschließend darüber informieren.

3.1 Coroutinen

Coroutinen bieten die grundlegendste Unterstützung für das asynchrone Programmiermodell von Python. Betrachten Sie das folgende Beispielprogramm:

import asyncio

async def func():
	print('Hello World, Asynchronous.')
	return 114514

async def main():
	res = await func()
	print(f'main func executed.{res}')
	
>>> func() # 协程函数的调用结果是一个协程对象
<coroutine object func at 0x000001D9C01C59C0>

>>> await func() # 通过await关键字可以执行协程对象,并获取真正的返回值。
Hello World, Asynchronous.
114514

>>> asyncio.run(main()) # 隐式创建新的事件循环,并执行协程对象。
Hello World, Asynchronous.
main func executed.114514

Wir asynchaben zwei über Schlüsselwörter deklariert 协程函数. Das Ergebnis eines Aufrufs einer Coroutine-Funktion ist eins 协程对象. Das Coroutine-Objekt kann über Schlüsselwörter awaitausgeführt und der tatsächliche Rückgabewert abgerufen werden. Es ist zu beachten, dass die Funktion selbst ebenfalls eine Coroutine-Funktion sein muss, wenn innerhalb einer Funktion eine andere Coroutine-Funktion asynchron aufgerufen werden muss . Hier ist eine Coroutine-Funktion, damit sie die Funktion mainaufrufen kann .func

Wir haben bereits erwähnt, dass Benutzer Coroutinen an die Ereignisschleife übergeben können, um die Planung zu verwalten. awaitDas Prinzip ist so. Wenn sich unter dem aktuellen Thread eine Ereignisschleife im laufenden Zustand befindet, wird das Coroutine-Objekt zur Planung an diesen übergeben. Wenn nicht, wird eine neue Ereignisschleife erstellt und aktiviert.

In ähnlicher Weise asyncio.run()kann die Coroutine auch über die Ereignisschleife ausgeführt werden. Der Unterschied besteht darin, dass asyncio.run()sie als Einstiegspunkt der Funktion verwendet werden soll, wodurch die Erstellung einer neuen Ereignisschleife erzwungen wird . Daher ist diese Methode verboten, wenn sie vorhanden ist andere aktive Ereignisschleifen im aktuellen Thread. Andernfalls wird es ausgelöst RuntimeError.

3.2 Erwartete Inhalte

Objekte , die über Schlüsselwörter empfangen und verarbeitet werden können await, werden als erwartbare Objekte bezeichnet. Solche Objekte können auch von der Ereignisschleife empfangen und ausgeführt werden. Obwohl wir __await__das erwartete Objekt durch die Implementierung von Methoden anpassen können, wird dies im Allgemeinen nicht empfohlen. In den meisten Fällen verwenden wir die drei von Python bereitgestellten Arten von wartbaren Objekten Coroutine: , Taskund Future.

Durch das Einkapseln von CoroutineObjekten als TaskObjekte können umfangreichere Versandfunktionen erhalten werden. Das Folgende ist ein Beispielprogramm:

async def func():
	print('func executed.')

# 通过asyncio将func()协程对象封装为任务对象
task1 = asyncio.create_task(func(), name='task 01')

# 等价于通过事件循环
loop = asyncio.get_event_loop()
task2 = loop.create_task(func(), name='task 02')

Im Gegensatz zu CoroutinesObjekten, die nach der Erstellung über die Ereignisschleife ausgeführt werden müssen, Taskwird das Objekt selbst über die Ereignisschleife erstellt. Dies bedeutet, dass create_taskdie interne Coroutine beim Ausführen bereits ausgeführt wird.

FutureEs handelt sich um ein erwartbares Objekt, das eine API auf niedrigerer Ebene bereitstellt, die das Ergebnis einer asynchronen Operation darstellt und auch zum Binden einer Coroutine verwendet werden kann, deren Ausführung noch nicht abgeschlossen ist. TaskDie geerbten FutureEigenschaften und Methoden Coroutinestellen zusammen damit eine übergeordnete asynchrone API bereit. Im Allgemeinen wird nicht empfohlen, Futureasynchrone Aufrufe direkt zu erstellen und durchzuführen. FutureDer Inhalt dazu wird in einem späteren Artikel erläutert.

3.3 Aufgabenerstellung und -stornierung

TaskEs ist ein wichtiges Werkzeug in der asynchronen Python-Programmierung. Wir können Taskden aktuellen Zustand der Coroutine erfahren und einen bestimmten Planungsbereich durchführen.

3.3.1 Erstellen

asyncioDie Methode zum Erstellen einer Aufgabe kann, wie im Code in 3.2 gezeigt, über die Bibliothek oder direkt über die Ereignisschleife erfolgen , was im Wesentlichen gleich ist. Hinweis: Stellen Sie sicher, dass Sie in , einen Verweis auf diesen Rückgabewert create_taskspeichern . TaskDa die Ereignisschleife Tasknach dem Empfang nur einen schwachen Verweis auf das Objekt beibehält und der Rückgabewert nicht ordnungsgemäß beibehalten wird, kann die Aufgabe jederzeit durch Müll gesammelt werden , unabhängig davon, ob die der Aufgabe entsprechende Coroutine ausgeführt wird oder nicht.

3.3.2 Stornierung

Die von der Aufgabe gekapselte Coroutine wird zur Ausführung durch die Ereignisschleife eingeplant. Die Ereignisschleife führt jeweils nur eine Coroutine aus. Wenn eine Coroutine das Ausführungsrecht übergibt, plant und bestimmt die Ereignisschleife die nächste auszuführende Coroutine. Bevor die Ausführung einer Coroutine endet, können wir Task.cancel()ihre Ausführung über die Methode abbrechen, wie im folgenden Code gezeigt.

import asyncio

# 定义一个协程函数
async def func():
	count = 0
	while True:
		await asyncio.sleep(1)
		print(f'awake {count := count + 1} time(s).')

# 创建任务
task1 = asyncio.create_task(func(), name="task1")
task2 = asyncio.create_task(func(), name="task2")

task1.cancel() # 取消task1的执行

cancel()Das Prinzip besteht darin, +1 zur Zielaufgabe hinzuzufügen 取消请求计数. Wenn im nächsten Zyklus der Ereignisschleife die Anzahl der Abbruchanforderungen der Coroutine größer als 0 ist, wird eine an die Coroutine-Funktion übergeben, um CancelledErrorderen weitere Ausführung zu beenden.

Bevor wir es also offiziell in die Coroutine werfen CancelledError, haben wir die Möglichkeit, die Methode zum Zurückziehen der Abbruchanforderung der Aufgabe zu verwenden uncancel(), wodurch die Abbruchanforderung -1 gezählt wird. Im obigen Beispiel können wir:

# 交出执行权
task1.cancel()    # cancel_count += 1, curr = 1
task1.cancel()    # cancel_count += 1, curr = 2
task1.uncancel()  # cancel_count -= 1, curr = 1
task1.uncancel()  # cancel_count -= 1, curr = 0
# 下次事件循环,由于取消请求计数为0,不会取消task1的执行

Selbst wenn CancelledErrordie Coroutine-Funktion tatsächlich übergeben wird, können wir try... except...die Ausnahme natürlich auch über die Anweisung abfangen, um eine Unterbrechung der Coroutine-Funktion zu vermeiden. Die Aufgabe wird jedoch weiterhin als abgebrochen markiert und wir müssen weiterhin die uncancel()Anfrage zum Rückgängigmachen des Abbruchs aufrufen, wenn die Ereignisschleife fortgesetzt werden soll.

Alternativ können wir es mit asyncio.shield()will umschließen Task, was ebenfalls verhindert, dass die Aufgabe abgebrochen wird.

task = asyncio.create_task(func())
await asyncio.shield(task)

# 由于如果直接await协程的话,协程是无法被取消的,因此上面的操作等价于
await func()

Darüber hinaus können wir bei Aufgaben, die normalerweise ausgeführt werden, anhand des Ergebnisses der Aufgabenausführung Task.done()beurteilen, ob die Aufgabe beendet ist . Wenn das Ergebnis nicht verfügbar ist, wird es geworfen , und wenn die Aufgabe, die das Ergebnis erhalten hat, abgebrochen wurde, wird es abgebrochen wird geworfen .Task.result()InvalidStateErrorCancelledError

3.4 Ruhezustand, Auszeit, Warten

3.4.1 Schlaf

asyncio.sleep()ist eine Funktion, die wir oft verwenden. Dadurch wird eine Coroutine für die von uns angegebene Anzahl von Sekunden in den Ruhezustand versetzt, ähnlich time.sleep()der Methode im synchronen Programmiermodell. Der Unterschied besteht darin, dass asyncio.sleep()der Thread nicht blockiert wird, sondern die aktuelle Coroutine das Ausführungsrecht übergibt.

Diese Funktion ist sehr nützlich. Wie oben erwähnt, kann die Ereignisschleife jeweils nur eine Coroutine ausführen. Während der normalen Ausführung einer Coroutine wird das Ausführungsrecht nicht übergeben, es sei denn, es trifft auf E/A-Vorgänge yield. awaitDies führt dazu, dass andere Coroutine-Aufgaben nicht ausgeführt werden. Und selbst wenn die Anzahl der Sekunden auf 0 gesetzt ist, kann die Coroutine durch asyncio.sleep()die Methode das Ausführungsrecht sofort übergeben und auf die nächste Planung der Ereignisschleife warten.

3.4.2 Zeitüberschreitung

Um zu verhindern, dass asynchrone Aufgaben wie asynchrone Netzwerkanforderungen und asynchrone E/A-Vorgänge zu lange dauern und die Coroutine nicht normal ausgeführt werden kann, können wir den Timeout-Mechanismus verwenden, um die Ausführung der Coroutine innerhalb eines Zeitlimits zu planen.

asyncio.timeoutIst ein asynchroner Kontextmanager, der eine Zeitüberschreitung festlegen kann. In diesem asynchronen Kontext wird die Coroutine-Ausführung ausgelöst, sobald eine Zeitüberschreitung auftritt TimeError. Bei den Zeitdaten handelt es sich hier nicht um ein Zeitintervall, sondern um die verstrichene Zeit seit Beginn der aktuellen Ereignisschleife. Wenn wir die Zeit nicht kennen, zu der wir den Timeout-Kontext aktivieren, können wir ihn vorübergehend auf None(kein Timeout) setzen. Verwenden Sie nach Eingabe des Kontexts time()die Methode des Ereignisschleifenobjekts, um die verstrichene Laufzeit abzurufen, und reschedule()planen Sie dann das Timeout über die Methode neu.

async def main():
    try:
        # 启动异步超时上下文管理器
        async with asyncio.timeout(None) as cm:
            # 获取当前事件循环已运行时间,并计算超时时刻
            new_deadline = get_running_loop().time() + 10
            # 重新规划超时
            cm.reschedule(new_deadline)
			# 执行协程
            await long_running_task()
    except TimeoutError:
    	# 捕获超时异常,同时协程得以继续运行
        pass
	# 通过上下文管理器的expired方法判断是否发生过超时
    if cm.expired():
        print("发生超时.")

Die Ereignisschleife initialisiert beim Start eine monotone Uhr und aktualisiert sie bei jeder Schleifeniteration. Bei jeder Schleifeniteration prüft die Ereignisschleife die Differenz zwischen der aktuellen Zeit und der vorherigen Schleifeniteration und addiert sie zur monotonen Uhr, um die neueste Zeit zu erhalten. Wenn in der obigen Timeout-Einstellung der Zeitwert kleiner als ist loop.time(), wird das Timeout sofort ausgelöst, wenn die Ereignisschleife iteriert.

Wenn eine Zeitüberschreitung auftritt, werden alle nicht abgeschlossenen Aufgaben im Kontext abgebrochen und die geworfenen Aufgaben CancelledErrorwerden in TimeoutErroreinheitliche Würfe umgewandelt.

3.4.3 Warten

Zusätzlich zum groben Timeout-Mechanismus können wir auch asyncio.wait()auf einen Stapel von Aufgaben warten. Nach dem Timeout werden zwei Sätze abgeschlossener und nicht abgeschlossener Aufgaben zurückgegeben, was für uns praktisch ist, sie separat zu verarbeiten.

import asyncio
async def func(delay: int):
	await asyncio.sleep(delay)
# 超时时间设置为5,对于执行时间1~10的10个协程来说,会有一半完成,另一半未完成,这两个集合都会返回
done, pending = await asyncio.wait([asyncio.create_task(func(i)) for i in range(1, 11)], timeout=5)
print(len(done), len(pending)) # 5 5

Zusätzlich zum Festlegen der Wartezeitlimitregeln können Sie natürlich auch return_whenandere Regeln über Parameter festlegen. Im Wesentlichen können die folgenden drei Konstanten übergeben werden:

Parameterwert beschreiben
FIRST_COMPLETED Rückkehr, wenn man fertig ist
FIRST_EXCEPTION Wenn in der ausgeführten Aufgabe eine Ausnahme ausgelöst wird, wird diese sofort zurückgegeben, andernfalls entspricht sie ALL_COMPLETED
ALLE_ABGESCHLOSSEN Wird zurückgegeben, wenn alle Aufgaben ausgeführt oder abgebrochen wurden

Im obigen Beispiel lautet das Ergebnis, das durch Anwendung der oben genannten drei Regeln erzielt wird:

done, pending = await asyncio.wait([asyncio.create_task(func(i)) for i in range(1, 11)], return_when=asyncio.FIRST_COMPLETED)
# FIRST_COMPLETED 1已完成 9未完成
# FIRST_EXCEPTION 10已完成 0未完成
# ALL_COMPLETED   10已完成 0未完成

3.5 Thread-Zuordnung

Obwohl für die synchronen Methoden in Python derzeit asynchrone Versionen zur Verfügung stehen, die von Entwicklern verwendet werden können. Was aber, wenn Sie auf eine synchrone Methode stoßen, die in Ihrer Coroutine-Funktion aufgerufen werden muss? Nach dem Aufruf wird der Thread, in dem sich die Coroutine befindet, direkt blockiert, und die zuvor besprochene Ereignisschleife, Coroutine-Effizienz usw. kommt nicht in Frage.

Um also zu verhindern, dass der wichtige Thread, in dem sich die Coroutine befindet, blockiert wird, können wir einen Kompromiss eingehen, asyncio.to_thread()die Synchronisationsmethode über die Methode in eine Coroutine einbinden und einen anderen Thread erstellen, um sie auszuführen.

import time
import asyncio
# 定义同步方法
def blocking_io():
    print(f"{time.strftime('%X')} 阻塞开始")
    # 使用time.sleep()来指代任意的同步方法,例如IO、网络请求、文件读写操作等
    time.sleep(1)
    print(f"{time.strftime('%X')} 阻塞结束")
    
async def main():
    print(f"协程从 {time.strftime('%X')} 开始执行")
    await asyncio.gather(
    	# 使用asyncio.to_thread封装一个同步函数,该方法返回一个协程
        asyncio.to_thread(blocking_io),
        asyncio.sleep(1))
        
    print(f"所有协程到 {time.strftime('%X')} 执行结束")
    
asyncio.run(main())
# 执行结果
#>>> 协程从 22:02:22 开始执行
#>>> 22:02:22 阻塞开始
#>>> 22:02:23 阻塞结束
#>>> 所有协程到 22:02:23 执行结束

Während der Thread-Zuweisung werden alle an die Coroutine-Funktion übergebenen Parameter an einen anderen Thread übergeben. Darüber hinaus werden die Kontextvariablen zwischen Threads ursprünglich nicht gemeinsam genutzt, sondern den durch Thread-Zuweisung erstellten Threads werden aktiv die Kontextvariablen des ursprünglichen Threads übergeben. Dadurch wird sichergestellt, dass die Coroutine-Funktion in einem anderen Thread korrekt ausgeführt werden kann.

Es kann festgestellt werden, dass der Thread, in dem sich die Coroutine befindet, nicht blockiert ist und das Programm insgesamt 1 Sekunde benötigt und wie geplant ausgeführt wird. Es ist zu beachten, dass aufgrund des Einflusses von GIL to_thread()nur die Leistung E/A-intensiver Synchronisationsmethoden verbessert werden kann , während bei CPU-intensiven Synchronisationsmethoden die CPU jeweils nur einen Thread ausführen kann, also auch wenn der Block vorhanden ist auf andere Threads übertragenEs hat auch keine offensichtliche Auswirkung.

Wenn unsere Programmierumgebung selbst über mehrere Threads verfügt, können wir nicht nur vorübergehend einen Thread erstellen, sondern auch asyncio.run_coroutine_threadsafedie Coroutine der Ereignisschleife eines bestimmten Threads zuweisen, um sie über die Methode auszuführen, um die flexible Planung der Coroutine zwischen mehreren Threads zu realisieren .

# loop1是来自另一个线程的正在运行的事件循环
future = asyncio.run_coroutine_threadsafe((coro:=asyncio.sleep(10)), loop)
# 返回值是一个Future对象,我们可以用与处理Task类似的方法处理它
try:
	# 获取结果,如果没执行完就等待一段时间
    result = future.result(timeout)
except TimeoutError:
	# 超时会触发超时异常,这个时候我们可以手动取消它
    print('协程执行超时,正在手动取消...')
    future.cancel()
except Exception as exc:
    print(f'协程函数抛出的其他异常: {exc!r}')
else:
	# 如果没问题的话,可以打印结果
    print(f'协程执行结果是{result!r}.')

Das Obige ist der gesamte Inhalt dieses Artikels, und mein Niveau ist begrenzt. Wenn es Unangemessenheiten gibt, können Sie mich gerne aufklären.

4. Referenzen

[1] Offizielle Python-Dokumentation: Coroutinen und Aufgaben.
[2] Offizielle Python-Dokumentation: Ereignisschleife

Ich denke du magst

Origin blog.csdn.net/qq_38236620/article/details/131151970
Empfohlen
Rangfolge