Utveckla med asyncio

Asynkron programmering skiljer sig från klassisk ”sekventiell” programmering.

På den här sidan listas vanliga misstag och fällor och det förklaras hur man undviker dem.

Felsökningsläge

Som standard körs asyncio i produktionsläge. För att underlätta utvecklingen har asyncio ett debuggläge.

Det finns flera sätt att aktivera felsökningsläget för asyncio:

Förutom att aktivera felsökningsläget bör du också överväga:

  • ställa in loggnivån för asyncio logger till logging.DEBUG, till exempel kan följande kodavsnitt köras vid start av applikationen:

    logging.basicConfig(level=logging.DEBUG)
    
  • konfigurera modulen warnings så att den visar ResourceWarning-varningar. Ett sätt att göra det är att använda kommandoradsalternativet -W default.

När felsökningsläget är aktiverat:

  • Många asyncio API:er som inte är trådlösa (t.ex. metoderna loop.call_soon() och loop.call_at()) ger upphov till ett undantag om de anropas från fel tråd.

  • I/O-väljarens exekveringstid loggas om det tar för lång tid att utföra en I/O-operation.

  • Återkallelser som tar längre tid än 100 millisekunder loggas. Attributet loop.slow_callback_duration kan användas för att ange den minsta exekveringstid i sekunder som anses vara ”långsam”.

Samtidighet och multithreading

En händelseslinga körs i en tråd (vanligtvis huvudtråden) och utför alla callbacks och Tasks i sin tråd. Medan en uppgift körs i händelseslingan kan inga andra uppgifter köras i samma tråd. När en uppgift utför ett await-uttryck avbryts den pågående uppgiften och händelseslingan utför nästa uppgift.

För att schemalägga en callback från en annan OS-tråd bör metoden loop.call_soon_threadsafe() användas. Exempel:

loop.call_soon_threadsafe(callback, *args)

Nästan alla asyncio-objekt är inte trådsäkra, vilket vanligtvis inte är ett problem om det inte finns kod som arbetar med dem utanför en Task eller en callback. Om det finns ett behov för sådan kod att anropa ett asyncio API på låg nivå, bör metoden loop.call_soon_threadsafe() användas, t.ex.:

loop.call_soon_threadsafe(fut.cancel)

För att schemalägga ett coroutine-objekt från en annan OS-tråd bör funktionen run_coroutine_threadsafe() användas. Den returnerar en concurrent.futures.Future för att komma åt resultatet:

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()

För att hantera signaler måste händelseslingan köras i huvudtråden.

Metoden loop.run_in_executor() kan användas med en concurrent.futures.ThreadPoolExecutor eller InterpreterPoolExecutor för att köra blockerande kod i en annan OS-tråd utan att blockera den OS-tråd som händelseslingan körs i.

Det finns för närvarande inget sätt att schemalägga coroutines eller callbacks direkt från en annan process (t.ex. en process som startats med multiprocessing). Avsnittet Metoder för händelseslingor listar API:er som kan läsa från rör och titta på filbeskrivare utan att blockera händelseslingan. Dessutom ger asyncios API:er Subprocess ett sätt att starta en process och kommunicera med den från händelseslingan. Slutligen kan den tidigare nämnda loop.run_in_executor()-metoden också användas med en concurrent.futures.ProcessPoolExecutor för att exekvera kod i en annan process.

Körning av blockerande kod

Blockerande (CPU-bunden) kod bör inte anropas direkt. Om en funktion t.ex. utför en CPU-intensiv beräkning under 1 sekund, kommer alla samtidiga asyncio Tasks och IO-operationer att försenas med 1 sekund.

En executor kan användas för att köra en uppgift i en annan tråd, inklusive i en annan tolk, eller till och med i en annan process för att undvika att blockera OS-tråden med händelseslingan. Se metoden loop.run_in_executor() för mer information.

Loggning

asyncio använder modulen logging och all loggning sker via loggern "asyncio".

Standardloggnivån är logging.INFO, som enkelt kan justeras:

logging.getLogger("asyncio").setLevel(logging.WARNING)

Nätverksloggning kan blockera händelseslingan. Vi rekommenderar att du använder en separat tråd för att hantera loggar eller använder icke-blockerande IO. Se till exempel Hantering av handläggare som blockerar.

Upptäck aldrig efterlängtade coroutines

När en coroutine-funktion anropas, men inte väntar (t.ex. coro() istället för await coro()) eller coroutinen inte schemaläggs med asyncio.create_task(), kommer asyncio att avge en RuntimeWarning:

import asyncio

async def test():
    print("never scheduled")

async def main():
    test()

asyncio.run(main())

Utgång:

test.py:7: RuntimeWarning: coroutine 'test' was never awaited
  test()

Utdata i felsökningsläge:

test.py:7: RuntimeWarning: Coroutine 'test' var aldrig väntad
Coroutine skapad vid (senaste anropet senast)
  Fil "../t.py", rad 9, i <module>
    asyncio.run(main(), debug=True)

 < .. >

  Fil "../t.py", rad 7, i main
    test()
  test()

Den vanliga lösningen är att antingen vänta på coroutinen eller anropa funktionen asyncio.create_task():

async def main():
    await test()

Upptäck undantag som aldrig hämtats

Om en Future.set_exception() anropas men Future-objektet aldrig blir väntat på, kommer undantaget aldrig att spridas till användarkoden. I detta fall skulle asyncio skicka ut ett loggmeddelande när Future-objektet samlas in.

Exempel på ett ohanterat undantag:

import asyncio

async def bug():
    raise Exception("not consumed")

async def main():
    asyncio.create_task(bug())

asyncio.run(main())

Utgång:

Uppgiftsundantaget hämtades aldrig
i framtiden: <Uppgift slutförd coro=<bug() utförd, definierad vid test.py:3>
  exception=Undantag('inte förbrukad')>

Traceback (senaste anropet senast):
  Fil "test.py", rad 4, i bug
    raise Undantag("inte förbrukad")
Undantag: inte förbrukad

Aktivera felsökningsläget för att få en spårning av var uppgiften skapades:

asyncio.run(main(), debug=True)

Utdata i felsökningsläge:

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