"Spara e dimentica" Python async / await


115

A volte è necessario eseguire un'operazione asincrona non critica, ma non voglio aspettare che venga completata. Nell'implementazione coroutine di Tornado è possibile "sparare e dimenticare" una funzione asincrona semplicemente omettendo la yieldparola chiave.

Ho cercato di capire come "sparare e dimenticare" con la nuova sintassi async/ awaitrilasciata in Python 3.5. Ad esempio, uno snippet di codice semplificato:

async def async_foo():
    print("Do some stuff asynchronously here...")

def bar():
    async_foo()  # fire and forget "async_foo()"

bar()

Quello che succede però è che bar()non viene mai eseguito e invece otteniamo un avviso di runtime:

RuntimeWarning: coroutine 'async_foo' was never awaited
  async_foo()  # fire and forget "async_foo()"

Relazionato? stackoverflow.com/q/32808893/1639625 In effetti, penso che sia un duplicato, ma non voglio duplicarlo istantaneamente. Qualcuno può confermare?
tobias_k

3
@tobias_k, non credo sia duplicato. La risposta al link è troppo ampia per essere una risposta a questa domanda.
Mikhail Gerasimov

2
(1) il tuo processo "principale" continua a funzionare all'infinito? Oppure (2) vuoi lasciare che il tuo processo muoia ma permettere che le attività dimenticate continuino il loro lavoro? Oppure (3) preferisci che il tuo processo principale aspetti le attività dimenticate appena prima di terminare?
Julien Palard

Risposte:


170

UPD:

Sostituisci asyncio.ensure_futurecon asyncio.create_taskovunque se stai usando Python> = 3.7 È un modo più nuovo e più carino per generare attività .


asyncio.Task "sparare e dimenticare"

Secondo la documentazione di Python asyncio.Taskè possibile avviare alcune coroutine da eseguire "in background" . L'attività creata dalla asyncio.ensure_future funzione non bloccherà l'esecuzione (quindi la funzione tornerà immediatamente!). Sembra un modo per "sparare e dimenticare" come richiesto.

import asyncio


async def async_foo():
    print("async_foo started")
    await asyncio.sleep(1)
    print("async_foo done")


async def main():
    asyncio.ensure_future(async_foo())  # fire and forget async_foo()

    # btw, you can also create tasks inside non-async funcs

    print('Do some actions 1')
    await asyncio.sleep(1)
    print('Do some actions 2')
    await asyncio.sleep(1)
    print('Do some actions 3')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Produzione:

Do some actions 1
async_foo started
Do some actions 2
async_foo done
Do some actions 3

Cosa succede se le attività vengono eseguite dopo il completamento del ciclo di eventi?

Nota che asyncio si aspetta che l'attività venga completata nel momento in cui il ciclo di eventi è completato. Quindi, se passerai main()a:

async def main():
    asyncio.ensure_future(async_foo())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(0.1)
    print('Do some actions 2')

Riceverai questo avviso al termine del programma:

Task was destroyed but it is pending!
task: <Task pending coro=<async_foo() running at [...]

Per evitare ciò, puoi semplicemente attendere tutte le attività in sospeso dopo il completamento del ciclo di eventi:

async def main():
    asyncio.ensure_future(async_foo())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(0.1)
    print('Do some actions 2')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    # Let's also finish all running tasks:
    pending = asyncio.Task.all_tasks()
    loop.run_until_complete(asyncio.gather(*pending))

Uccidi i compiti invece di aspettarli

A volte non si desidera attendere il completamento delle attività (ad esempio, alcune attività potrebbero essere create per essere eseguite all'infinito). In tal caso, puoi semplicemente cancellarli () invece di aspettarli:

import asyncio
from contextlib import suppress


async def echo_forever():
    while True:
        print("echo")
        await asyncio.sleep(1)


async def main():
    asyncio.ensure_future(echo_forever())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(1)
    print('Do some actions 2')
    await asyncio.sleep(1)
    print('Do some actions 3')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    # Let's also cancel all running tasks:
    pending = asyncio.Task.all_tasks()
    for task in pending:
        task.cancel()
        # Now we should await task to execute it's cancellation.
        # Cancelled task raises asyncio.CancelledError that we can suppress:
        with suppress(asyncio.CancelledError):
            loop.run_until_complete(task)

Produzione:

Do some actions 1
echo
Do some actions 2
echo
Do some actions 3
echo

Ho copiato e passato il primo blocco e l'ho semplicemente eseguito dalla mia parte e per qualche motivo ho ottenuto: line 4 async def async_foo (): ^ Come se ci fosse qualche errore di sintassi con la definizione della funzione sulla riga 4: "async def async_foo ( ):" Mi sto perdendo qualcosa?
Gil Allen

3
@GilAllen questa sintassi funziona solo in Python 3.5+. Python 3.4 necessita della vecchia sintassi (vedere docs.python.org/3.4/library/asyncio-task.html ). Python 3.3 e versioni precedenti non supportano affatto asyncio.
Mikhail Gerasimov

Come uccideresti le attività in un thread? ... ̣Ho un thread che crea alcune attività e voglio uccidere tutte quelle in sospeso quando il thread muore nel suo stop()metodo.
Sardathrion - contro l'abuso di SE il

@Sardathrion Non sono sicuro che l'attività punti da qualche parte sul thread in cui è stata creata, ma nulla ti impedisce di rintracciarli manualmente: ad esempio, aggiungi tutte le attività create nel thread a un elenco e quando arriva il momento annullale come spiegato sopra.
Mikhail Gerasimov

2
Nota che "Task.all_tasks () è deprecato da Python 3.7, usa invece asyncio.all_tasks ()"
Alexis

12

Grazie Sergey per la breve risposta. Ecco la versione decorata dello stesso.

import asyncio
import time

def fire_and_forget(f):
    def wrapped(*args, **kwargs):
        return asyncio.get_event_loop().run_in_executor(None, f, *args, *kwargs)

    return wrapped

@fire_and_forget
def foo():
    time.sleep(1)
    print("foo() completed")

print("Hello")
foo()
print("I didn't wait for foo()")

produce

>>> Hello
>>> foo() started
>>> I didn't wait for foo()
>>> foo() completed

Nota: controlla la mia altra risposta che fa lo stesso usando thread semplici.


Ho riscontrato un rallentamento sostanziale dopo aver utilizzato questo approccio creando ~ 5 piccole attività spara e dimentica al secondo. Non utilizzarlo in produzione per un'attività a esecuzione prolungata. Mangerà la tua CPU e la tua memoria!
pir

10

Questa non è un'esecuzione completamente asincrona, ma forse run_in_executor () è adatto a te.

def fire_and_forget(task, *args, **kwargs):
    loop = asyncio.get_event_loop()
    if callable(task):
        return loop.run_in_executor(None, task, *args, **kwargs)
    else:    
        raise TypeError('Task must be a callable')

def foo():
    #asynchronous stuff here


fire_and_forget(foo)

3
Bella risposta concisa. Vale la pena notare che l' executorimpostazione predefinita per la chiamata concurrent.futures.ThreadPoolExecutor.submit(). Dico perché la creazione di thread non è gratuita; Sparare e dimenticare 1000 volte al secondo probabilmente metterà a dura prova la gestione dei thread
Brad Solomon

Sì. Non ho tenuto conto del tuo avvertimento e ho riscontrato un rallentamento sostanziale dopo aver utilizzato questo approccio creando ~ 5 piccoli compiti spara e dimentica al secondo. Non utilizzarlo in produzione per un'attività a esecuzione prolungata. Mangerà la tua CPU e la tua memoria!
pir

3

Per qualche motivo, se non sei in grado di utilizzare, asyncioecco l'implementazione che utilizza thread semplici. Controlla le mie altre risposte e anche la risposta di Sergey.

import threading

def fire_and_forget(f):
    def wrapped():
        threading.Thread(target=f).start()

    return wrapped

@fire_and_forget
def foo():
    time.sleep(1)
    print("foo() completed")

print("Hello")
foo()
print("I didn't wait for foo()")

Se abbiamo solo bisogno di questa funzionalità fire_and_forget e nient'altro da asyncio, sarebbe comunque meglio usare asyncio? Quali sono i vantaggi?
pir
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.