"Dispara y olvida" python async / await

115

A veces hay alguna operación asincrónica no crítica que debe suceder, pero no quiero esperar a que se complete. En la implementación de corrutinas de Tornado, puede "disparar y olvidar" una función asincrónica simplemente omitiendo la yieldpalabra clave.

He estado tratando de averiguar cómo "disparar y olvidar" con la nueva sintaxis async/ awaitlanzada en Python 3.5. Por ejemplo, un fragmento de código simplificado:

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

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

bar()

Sin embargo, lo que sucede es que bar()nunca se ejecuta y, en cambio, recibimos una advertencia de tiempo de ejecución:

RuntimeWarning: coroutine 'async_foo' was never awaited
  async_foo()  # fire and forget "async_foo()"
Mike N
fuente
¿Relacionado? stackoverflow.com/q/32808893/1639625 De hecho, creo que es un duplicado, pero no quiero engañarlo instantáneamente. ¿Alguien puede confirmar?
tobias_k
3
@tobias_k, no creo que esté duplicado. La respuesta en el enlace es demasiado amplia para responder a esta pregunta.
Mikhail Gerasimov
2
¿(1) su proceso "principal" continúa ejecutándose para siempre? O (2) ¿desea permitir que su proceso muera pero permitir que las tareas olvidadas continúen su trabajo? O (3) ¿prefiere que su proceso principal espere las tareas olvidadas justo antes de finalizar?
Julien Palard

Respuestas:

170

Upd:

Reemplace asyncio.ensure_futurecon en asyncio.create_tasktodas partes si está usando Python> = 3.7 Es una forma más nueva y agradable de generar tareas .


asyncio. Tarea para "disparar y olvidar"

De acuerdo con los documentos de Python asyncio.Task, es posible iniciar algunas corrutinas para ejecutar "en segundo plano" . La tarea creada por la asyncio.ensure_future función no bloqueará la ejecución (¡por lo tanto, la función regresará inmediatamente!). Esto parece una forma de "disparar y olvidar" como lo solicitó.

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

Salida:

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

¿Qué sucede si las tareas se ejecutan después de que se completa el ciclo de eventos?

Tenga en cuenta que asyncio espera que la tarea se complete en el momento en que se complete el ciclo de eventos. Entonces, si cambia 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')

Recibirá esta advertencia después de que finalice el programa:

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

Para evitarlo, puede esperar todas las tareas pendientes una vez completado el ciclo de eventos:

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

Mata tareas en lugar de esperarlas

A veces, no desea esperar a que se realicen tareas (por ejemplo, es posible que algunas tareas se creen para ejecutarse para siempre). En ese caso, puede cancelarlos () en lugar de esperarlos:

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)

Salida:

Do some actions 1
echo
Do some actions 2
echo
Do some actions 3
echo
Mikhail Gerasimov
fuente
Copié y pasé el primer bloque y simplemente lo ejecuté en mi extremo y por alguna razón obtuve: línea 4 async def async_foo (): ^ Como si hubiera algún error de sintaxis con la definición de función en la línea 4: "async def async_foo ( ):" ¿Me estoy perdiendo de algo?
Gil Allen
3
@GilAllen esta sintaxis solo funciona en Python 3.5+. Python 3.4 necesita una sintaxis antigua (consulte docs.python.org/3.4/library/asyncio-task.html ). Python 3.3 y versiones anteriores no admiten asyncio en absoluto.
Mikhail Gerasimov
¿Cómo matarías las tareas en un hilo?… ̣Tengo un hilo que crea algunas tareas y quiero eliminar todas las pendientes cuando el hilo muera en su stop()método.
Sardathrion - contra el abuso SE
@Sardathrion No estoy seguro de si la tarea apunta en algún lugar del hilo en el que se creó, pero nada le impide rastrearlas manualmente: por ejemplo, simplemente agregue todas las tareas creadas en el hilo a una lista y, cuando llegue el momento, cancele las explicaciones. encima.
Mikhail Gerasimov
2
Tenga en cuenta que "Task.all_tasks () está obsoleto desde Python 3.7, use asyncio.all_tasks () en su lugar"
Alexis
12

Gracias Sergey por la sucinta respuesta. Aquí está la versión decorada del mismo.

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: Verifique mi otra respuesta que hace lo mismo usando hilos simples.

nehem
fuente
Experimenté una desaceleración sustancial después de usar este enfoque creando ~ 5 pequeñas tareas de disparar y olvidar por segundo. No use esto en producción para una tarea de larga duración. ¡Se comerá tu CPU y tu memoria!
pir
10

Esta no es una ejecución completamente asincrónica, pero quizás run_in_executor () sea ​​adecuado para usted.

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)
Sergey Gornostaev
fuente
3
Buena respuesta concisa. Vale la pena señalar que el executorpredeterminado para llamar concurrent.futures.ThreadPoolExecutor.submit(). Lo menciono porque crear hilos no es gratis; disparar y olvidar 1000 veces por segundo probablemente ejercerá una gran presión en la gestión de subprocesos
Brad Solomon
Sí. No hice caso de su advertencia y experimenté una desaceleración sustancial después de usar este enfoque, creando ~ 5 pequeñas tareas de disparar y olvidar por segundo. No use esto en producción para una tarea de larga duración. ¡Se comerá tu CPU y tu memoria!
pir
3

Por alguna razón, si no puede usar asyncio, aquí está la implementación con subprocesos simples. Compruebe mis otras respuestas y la respuesta de Sergey también.

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()")
nehem
fuente
Si solo necesitamos esta funcionalidad fire_and_forget y nada más de asyncio, ¿sería mejor usar asyncio? ¿Cuáles son los beneficios?
pir