“Dispare e esqueça” python async / await

115

Às vezes, há alguma operação assíncrona não crítica que precisa acontecer, mas eu não quero esperar que ela seja concluída. Na implementação de co-rotina do Tornado, você pode "disparar e esquecer" uma função assíncrona simplesmente omitindo a yieldpalavra-chave.

Tenho tentado descobrir como "disparar e esquecer" com a nova async/ awaitsintaxe lançada no Python 3.5. Por exemplo, um snippet de código simplificado:

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

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

bar()

O que acontece é que isso bar()nunca é executado e, em vez disso, recebemos um aviso de tempo de execução:

RuntimeWarning: coroutine 'async_foo' was never awaited
  async_foo()  # fire and forget "async_foo()"
Mike N
fonte
Relacionado? stackoverflow.com/q/32808893/1639625 Na verdade, acho que é uma duplicata, mas não quero martelá-la instantaneamente. Alguém pode confirmar?
tobias_k
3
@tobias_k, não acho que seja duplicado. A resposta no link é muito ampla para ser respondida para esta pergunta.
Mikhail Gerasimov
2
(1) o seu processo "principal" continua em execução para sempre? Ou (2) você quer permitir que seu processo morra, mas permitindo que tarefas esquecidas continuem seu trabalho? Ou (3) você prefere que seu processo principal espere por tarefas esquecidas antes de terminar?
Julien Palard

Respostas:

170

Upd:

Substitua asyncio.ensure_futurepor asyncio.create_taskqualquer lugar se estiver usando Python> = 3.7. É uma maneira mais nova e mais agradável de gerar tarefas .


asyncio. Peça para “disparar e esquecer”

De acordo com a documentação do python asyncio.Task, é possível iniciar alguma co-rotina para executar "em segundo plano" . A tarefa criada pela asyncio.ensure_future função não bloqueará a execução (portanto, a função retornará imediatamente!). Parece uma forma de “disparar e esquecer” conforme solicitado.

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

Resultado:

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

E se as tarefas estiverem sendo executadas após a conclusão do loop de evento?

Observe que asyncio espera que a tarefa seja concluída no momento em que o loop de eventos for concluído. Então, se você mudar main()para:

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

Você receberá este aviso após a conclusão do programa:

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

Para evitar isso, você pode apenas aguardar todas as tarefas pendentes após a conclusão do loop 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))

Mate tarefas em vez de esperá-las

Às vezes, você não quer esperar que as tarefas sejam realizadas (por exemplo, algumas tarefas podem ser criadas para serem executadas para sempre). Nesse caso, você pode simplesmente cancelá-los () em vez de aguardá-los:

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)

Resultado:

Do some actions 1
echo
Do some actions 2
echo
Do some actions 3
echo
Mikhail Gerasimov
fonte
Copiei e colei o primeiro bloco e simplesmente o executei no meu final e por algum motivo eu obtive: linha 4 async def async_foo (): ^ Como se houvesse algum erro de sintaxe com a definição da função na linha 4: "async def async_foo ( ):" Estou esquecendo de algo?
Gil Allen
3
@GilAllen, esta sintaxe funciona apenas no Python 3.5+. O Python 3.4 precisa de uma sintaxe antiga (consulte docs.python.org/3.4/library/asyncio-task.html ). Python 3.3 e abaixo não oferecem suporte a assíncio.
Mikhail Gerasimov
Como você mataria as tarefas em um thread? ... ̣ Eu tenho um thread que cria algumas tarefas e quero matar todas as pendentes quando o thread morrer em seu stop()método.
Sardathrion - contra abuso de SE
@Sardathrion Não tenho certeza se a tarefa aponta para algum lugar no thread em que foi criada, mas nada impede você de rastreá-los manualmente: por exemplo, basta adicionar todas as tarefas criadas no thread a uma lista e, quando chegar a hora, cancelá-las da maneira explicada acima.
Mikhail Gerasimov
2
Observe que "Task.all_tasks () está obsoleto desde Python 3.7, use asyncio.all_tasks () em vez"
Alexis
12

Obrigado, Sergey pela resposta sucinta. Aqui está a versão decorada do mesmo.

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

Produz

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

Nota: verifique minha outra resposta, que faz o mesmo usando tópicos simples.

Nehem
fonte
Eu experimentei uma desaceleração substancial depois de usar essa abordagem, criando ~ 5 pequenas tarefas do tipo "disparar e esquecer" por segundo. Não use isso na produção para uma tarefa de longa duração. Ele vai comer sua CPU e memória!
pir
10

Esta não é uma execução totalmente assíncrona, mas talvez run_in_executor () seja adequado para você.

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
fonte
3
Boa resposta concisa. É importante notar que o executorpadrão será chamar concurrent.futures.ThreadPoolExecutor.submit(). Menciono porque a criação de threads não é gratuita; disparar e esquecer 1000 vezes por segundo provavelmente colocará uma grande pressão no gerenciamento de thread
Brad Solomon
Sim. Não prestei atenção ao seu aviso e experimentei uma desaceleração substancial depois de usar essa abordagem, criando cerca de 5 pequenas tarefas disparar e esquecer por segundo. Não use isso na produção para uma tarefa de longa duração. Ele vai comer sua CPU e memória!
pir
3

Por alguma razão, se você não puder usar asyncio, aqui está a implementação usando threads simples. Verifique minhas outras respostas e a resposta de Sergey também.

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
fonte
Se precisarmos apenas dessa funcionalidade fire_and_forget e nada mais do asyncio, ainda seria melhor usar asyncio? Quais são os benefícios?
pir