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

Zusätzlich zur Aktivierung des Debug-Modus sollten Sie auch in Betracht ziehen

  • das Log-Level des asyncio-Loggers auf logging.DEBUG zu setzen, z. B. kann der folgende Code-Schnipsel beim Start der Anwendung ausgeführt werden

    logging.basicConfig(level=logging.DEBUG)
    
  • Konfigurieren des warnings-Moduls, um ResourceWarning-Warnungen anzuzeigen. Eine Möglichkeit, dies zu tun, ist die Verwendung der Befehlszeilenoption -W mit dem Wert default.

Wenn der Debug-Modus aktiviert ist

  • Viele nicht-thread-sichere asyncio-APIs (wie z. B. die Methoden loop.call_soon() und loop.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_duration kann 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