Entwicklung mit asyncio¶
Asynchrone Programmierung unterscheidet sich von klassischer "sequenzieller" Programmierung.
Diese Seite listet häufige Fehler und Fallen auf und erklärt, wie man sie vermeidet.
Debug-Modus¶
Standardmäßig läuft asyncio im Produktionsmodus. Um die Entwicklung zu erleichtern, verfügt asyncio über einen Debug-Modus.
Es gibt verschiedene Möglichkeiten, den asyncio-Debug-Modus zu aktivieren
Setzen der Umgebungsvariable
PYTHONASYNCIODEBUGauf1.Verwendung des Python-Entwicklungsmodus.
Übergabe von
debug=Trueanasyncio.run().Aufrufen von
loop.set_debug().
Zusätzlich zur Aktivierung des Debug-Modus sollten Sie auch in Betracht ziehen
das Log-Level des asyncio-Loggers auf
logging.DEBUGzu setzen, z. B. kann der folgende Code-Schnipsel beim Start der Anwendung ausgeführt werdenlogging.basicConfig(level=logging.DEBUG)
Konfigurieren des
warnings-Moduls, umResourceWarning-Warnungen anzuzeigen. Eine Möglichkeit, dies zu tun, ist die Verwendung der Befehlszeilenoption-Wmit dem Wertdefault.
Wenn der Debug-Modus aktiviert ist
Viele nicht-thread-sichere asyncio-APIs (wie z. B. die Methoden
loop.call_soon()undloop.call_at()) lösen eine Ausnahme aus, wenn sie von einem falschen Thread aus aufgerufen werden.Die Ausführungszeit des I/O-Selektors wird protokolliert, wenn eine I/O-Operation zu lange dauert.
Callbacks, die länger als 100 Millisekunden dauern, werden protokolliert. Das Attribut
loop.slow_callback_durationkann verwendet werden, um die minimale Ausführungsdauer in Sekunden festzulegen, die als "langsam" gilt.
Nebenläufigkeit und Multithreading¶
Eine Event-Schleife läuft in einem Thread (typischerweise dem Haupt-Thread) und führt alle Callbacks und Tasks in ihrem Thread aus. Während ein Task in der Event-Schleife läuft, können keine anderen Tasks im selben Thread ausgeführt werden. Wenn ein Task einen await-Ausdruck ausführt, wird der laufende Task angehalten, und die Event-Schleife führt den nächsten Task aus.
Um einen Callback von einem anderen Betriebssystem-Thread zu planen, sollte die Methode loop.call_soon_threadsafe() verwendet werden. Beispiel
loop.call_soon_threadsafe(callback, *args)
Fast alle asyncio-Objekte sind nicht thread-sicher, was normalerweise kein Problem darstellt, es sei denn, es gibt Code, der von außerhalb eines Tasks oder eines Callbacks damit arbeitet. Wenn solcher Code eine Low-Level-asyncio-API aufrufen muss, sollte die Methode loop.call_soon_threadsafe() verwendet werden, z. B.
loop.call_soon_threadsafe(fut.cancel)
Um ein Coroutine-Objekt von einem anderen Betriebssystem-Thread aus zu planen, sollte die Funktion run_coroutine_threadsafe() verwendet werden. Sie gibt ein concurrent.futures.Future zurück, um auf das Ergebnis zuzugreifen
async def coro_func():
return await asyncio.sleep(1, 42)
# Later in another OS thread:
future = asyncio.run_coroutine_threadsafe(coro_func(), loop)
# Wait for the result:
result = future.result()
Um Signale zu verarbeiten, muss die Event-Schleife im Haupt-Thread laufen.
Die Methode loop.run_in_executor() kann mit einem concurrent.futures.ThreadPoolExecutor oder InterpreterPoolExecutor verwendet werden, um blockierenden Code in einem anderen Betriebssystem-Thread auszuführen, ohne den Betriebssystem-Thread, in dem die Event-Schleife läuft, zu blockieren.
Es gibt derzeit keine Möglichkeit, Coroutinen oder Callbacks direkt von einem anderen Prozess aus zu planen (z. B. einem, der mit multiprocessing gestartet wurde). Der Abschnitt Event Loop Methods listet APIs auf, die aus Pipes lesen und File-Deskriptoren überwachen können, ohne die Event-Schleife zu blockieren. Darüber hinaus bieten die Subprocess-APIs von asyncio eine Möglichkeit, einen Prozess zu starten und von der Event-Schleife aus mit ihm zu kommunizieren. Schließlich kann die oben erwähnte Methode loop.run_in_executor() auch mit einem concurrent.futures.ProcessPoolExecutor verwendet werden, um Code in einem anderen Prozess auszuführen.
Ausführen blockierenden Codes¶
Blockierender (CPU-gebundener) Code sollte nicht direkt aufgerufen werden. Wenn eine Funktion beispielsweise 1 Sekunde lang eine CPU-intensive Berechnung durchführt, würden alle gleichzeitigen asyncio-Tasks und I/O-Operationen um 1 Sekunde verzögert.
Ein Executor kann verwendet werden, um eine Aufgabe in einem anderen Thread auszuführen, einschließlich in einem anderen Interpreter, oder sogar in einem anderen Prozess, um zu vermeiden, dass der Betriebssystem-Thread mit der Event-Schleife blockiert wird. Weitere Details finden Sie unter der Methode loop.run_in_executor().
Protokollierung¶
asyncio verwendet das Modul logging und alle Protokollierungen erfolgen über den Logger "asyncio".
Das Standard-Log-Level ist logging.INFO, das leicht angepasst werden kann
logging.getLogger("asyncio").setLevel(logging.WARNING)
Netzwerkprotokollierung kann die Event-Schleife blockieren. Es wird empfohlen, einen separaten Thread für die Protokollbehandlung zu verwenden oder nicht-blockierende I/O zu nutzen. Sehen Sie sich zum Beispiel Umgang mit blockierenden Handlern an.
Nie erwartete Coroutinen erkennen¶
Wenn eine Coroutine-Funktion aufgerufen, aber nicht erwartet wird (z. B. coro() anstelle von await coro()) oder die Coroutine nicht mit asyncio.create_task() geplant wird, gibt asyncio eine RuntimeWarning aus
import asyncio
async def test():
print("never scheduled")
async def main():
test()
asyncio.run(main())
Ausgabe
test.py:7: RuntimeWarning: coroutine 'test' was never awaited
test()
Ausgabe im Debug-Modus
test.py:7: RuntimeWarning: coroutine 'test' was never awaited
Coroutine created at (most recent call last)
File "../t.py", line 9, in <module>
asyncio.run(main(), debug=True)
< .. >
File "../t.py", line 7, in main
test()
test()
Die übliche Behebung besteht darin, entweder die Coroutine zu erwarten oder die Funktion asyncio.create_task() aufzurufen
async def main():
await test()
Nie abgerufene Ausnahmen erkennen¶
Wenn Future.set_exception() aufgerufen wird, aber das Future-Objekt nie erwartet wird, würde die Ausnahme niemals an den Benutzercode weitergegeben. In diesem Fall gibt asyncio eine Log-Meldung aus, wenn das Future-Objekt garbage collected wird.
Beispiel für eine unbehandelte Ausnahme
import asyncio
async def bug():
raise Exception("not consumed")
async def main():
asyncio.create_task(bug())
asyncio.run(main())
Ausgabe
Task exception was never retrieved
future: <Task finished coro=<bug() done, defined at test.py:3>
exception=Exception('not consumed')>
Traceback (most recent call last):
File "test.py", line 4, in bug
raise Exception("not consumed")
Exception: not consumed
Aktivieren Sie den Debug-Modus, um den Traceback zu erhalten, wo der Task erstellt wurde
asyncio.run(main(), debug=True)
Ausgabe im Debug-Modus
Task exception was never retrieved
future: <Task finished coro=<bug() done, defined at test.py:3>
exception=Exception('not consumed') created at asyncio/tasks.py:321>
source_traceback: Object created at (most recent call last):
File "../t.py", line 9, in <module>
asyncio.run(main(), debug=True)
< .. >
Traceback (most recent call last):
File "../t.py", line 4, in bug
raise Exception("not consumed")
Exception: not consumed