web-dev-qa-db-fra.com

"Feu et oublie" python async / wait

Parfois, une opération asynchrone non critique doit être exécutée, mais je ne veux pas attendre qu'elle se termine. Dans l'implémentation en coroutine de Tornado, vous pouvez "déclencher et oublier" une fonction asynchrone en omettant simplement le mot clé yield.

J'ai essayé de comprendre comment "déclencher et oublier" avec la nouvelle syntaxe async/await publiée dans Python 3.5. Par exemple, une version simplifiée extrait de code:

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

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

bar()

Cependant, ce qui se passe, c’est que bar() ne s’exécute jamais et nous obtenons à la place un avertissement à l’exécution:

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

Upd:

Remplacez asyncio.ensure_future Par asyncio.create_task Partout si vous utilisez Python> = 3.7. C'est plus récent et plus sympa pour générer une tâche .


asyncio.Tâche de "tirer et oublier"

Selon python docs pour asyncio.Task il est possible de démarrer un coroutine pour exécuter "en arrière-plan" . La tâche créée par asyncio.ensure_futurefonction ne bloquera pas l'exécution (donc la fonction retournera immédiatement!). Cela ressemble à un moyen de “ feu et oublie "comme tu l'as demandé.

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 __== '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Sortie:

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

Que se passe-t-il si les tâches s'exécutent après la fin de la boucle d'événement?

Notez que asyncio s'attend à ce que la tâche soit terminée au moment où la boucle d'événement est terminée. Donc, si vous changez main() en:

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

Vous recevrez cet avertissement une fois le programme terminé:

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

Pour éviter cela, vous pouvez simplement attendre toutes les tâches en attente après la fin de la boucle d'événement:

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 __== '__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))

Tuer des tâches au lieu de les attendre

Parfois, vous ne voulez pas attendre que des tâches soient effectuées (par exemple, certaines tâches peuvent être créées pour être exécutées indéfiniment). Dans ce cas, vous pouvez simplement les annuler () au lieu de les attendre:

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 __== '__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)

Sortie:

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

Ce n’est pas une exécution entièrement asynchrone, mais peut-être que run_in_executor () vous convient.

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

Merci Sergey pour la réponse succincte. Voici la version décorée de la même chose.

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

Produit

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