Diferença entre co-rotina e futuro / tarefa em Python 3.5?

100

Digamos que temos uma função fictícia:

async def foo(arg):
    result = await some_remote_call(arg)
    return result.upper()

Qual é a diferença entre:

coros = []
for i in range(5):
    coros.append(foo(i))

loop = get_event_loop()
loop.run_until_complete(wait(coros))

E:

from asyncio import ensure_future

futures = []
for i in range(5):
    futures.append(ensure_future(foo(i)))

loop = get_event_loop()
loop.run_until_complete(wait(futures))

Observação : o exemplo retorna um resultado, mas este não é o foco da pergunta. Quando o valor de retorno for importante, use em gather()vez de wait().

Independentemente do valor de retorno, estou procurando clareza sobre ensure_future(). wait(coros)e wait(futures)ambos executam as corrotinas, então quando e por que uma corrotina deve ser incluída ensure_future?

Basicamente, qual é a maneira certa (tm) de executar um monte de operações sem bloqueio usando Python 3.5 async?

Para obter crédito extra, e se eu quiser agrupar as chamadas? Por exemplo, preciso ligar some_remote_call(...)1000 vezes, mas não quero esmagar o servidor web / banco de dados / etc com 1000 conexões simultâneas. Isso pode ser feito com um thread ou pool de processos, mas existe uma maneira de fazer isso com asyncio?

malha
fonte

Respostas:

95

Uma co-rotina é uma função geradora que pode produzir valores e aceitar valores externos. O benefício de usar uma co-rotina é que podemos pausar a execução de uma função e retomá-la mais tarde. No caso de uma operação de rede, faz sentido pausar a execução de uma função enquanto esperamos pela resposta. Podemos usar o tempo para executar algumas outras funções.

Um futuro é como os Promiseobjetos de Javascript. É como um espaço reservado para um valor que se materializará no futuro. No caso mencionado acima, enquanto espera pela E / S da rede, uma função pode nos dar um contêiner, uma promessa de que preencherá o contêiner com o valor quando a operação for concluída. Nós nos agarramos ao objeto futuro e quando ele for cumprido, podemos chamar um método nele para recuperar o resultado real.

Resposta direta: Você não precisa, ensure_futurese não precisa dos resultados. Eles são bons se você precisar dos resultados ou recuperar exceções ocorridas.

Créditos extras: eu escolheria run_in_executore aprovaria uma Executorinstância para controlar o número máximo de trabalhadores.

Explicações e códigos de amostra

No primeiro exemplo, você está usando corrotinas. A waitfunção pega um monte de co-rotinas e as combina. Então wait()termina quando todas as co-rotinas estiverem esgotadas (concluído / terminado retornando todos os valores).

loop = get_event_loop() # 
loop.run_until_complete(wait(coros))

O run_until_completemétodo garantiria que o loop estivesse ativo até que a execução fosse concluída. Observe como você não está obtendo os resultados da execução assíncrona neste caso.

No segundo exemplo, você está usando a ensure_futurefunção para envolver uma co-rotina e retornar um Taskobjeto que é uma espécie de Future. A corrotina está programada para ser executada no loop de evento principal quando você chama ensure_future. O objeto futuro / tarefa retornado ainda não tem um valor, mas com o tempo, quando as operações de rede terminarem, o objeto futuro conterá o resultado da operação.

from asyncio import ensure_future

futures = []
for i in range(5):
    futures.append(ensure_future(foo(i)))

loop = get_event_loop()
loop.run_until_complete(wait(futures))

Portanto, neste exemplo, estamos fazendo a mesma coisa, exceto que estamos usando futuros em vez de apenas usar corrotinas.

Vejamos um exemplo de como usar asyncio / corroutines / futures:

import asyncio


async def slow_operation():
    await asyncio.sleep(1)
    return 'Future is done!'


def got_result(future):
    print(future.result())

    # We have result, so let's stop
    loop.stop()


loop = asyncio.get_event_loop()
task = loop.create_task(slow_operation())
task.add_done_callback(got_result)

# We run forever
loop.run_forever()

Aqui, usamos o create_taskmétodo no loopobjeto. ensure_futureiria agendar a tarefa no loop de evento principal. Este método nos permite agendar uma co-rotina em um loop que escolhermos.

Também vemos o conceito de adicionar um retorno de chamada usando o add_done_callbackmétodo no objeto de tarefa.

A Taské donequando a co-rotina retorna um valor, levanta uma exceção ou é cancelada. Existem métodos para verificar esses incidentes.

Escrevi algumas postagens de blog sobre esses tópicos que podem ajudar:

Claro, você pode encontrar mais detalhes no manual oficial: https://docs.python.org/3/library/asyncio.html

Masnun
fonte
3
Atualizei minha pergunta para ficar um pouco mais clara - se não precisar do resultado da co-rotina, ainda preciso usar ensure_future()? E se eu precisar do resultado, não posso simplesmente usar run_until_complete(gather(coros))?
knite
1
ensure_futureagenda a co-rotina a ser executada no loop de eventos. Então, eu diria que sim, é obrigatório. Mas é claro que você pode agendar as corrotinas usando outras funções / métodos também. Sim, você pode usar gather()- mas o collect irá esperar até que todas as respostas sejam coletadas.
Masnun de
5
@AbuAshrafMasnun @knite gathere waitrealmente agrupar as corrotinas fornecidas como tarefas usando ensure_future(veja as fontes aqui e aqui ). Portanto, não adianta usar de ensure_futureantemão e não tem nada a ver com obter os resultados ou não.
Vincent
8
@AbuAshrafMasnun @knite Além disso, ensure_futuretem um loopargumento, então não há razão para usar loop.create_taskover ensure_future. E run_in_executornão funcionará com corrotinas, um semáforo deve ser usado em seu lugar.
Vincent
2
@vincent há uma razão para usar create_taskover ensure_future, veja os documentos . Citaçãocreate_task() (added in Python 3.7) is the preferable way for spawning new tasks.
masi
24

Resposta simples

  • Invocar uma função de co-rotina ( async def) NÃO a executa. Ele retorna objetos de co-rotina, como a função de gerador retorna objetos de gerador.
  • await recupera valores de co-rotinas, ou seja, "chama" a co-rotina
  • eusure_future/create_task agende a co-rotina para executar no loop de eventos na próxima iteração (embora não espere que eles terminem, como um encadeamento daemon).

Alguns exemplos de código

Vamos primeiro esclarecer alguns termos:

  • função de co-rotina, a que você async defestá;
  • objeto de co-rotina, o que você obtém quando "chama" uma função de co-rotina;
  • tarefa, um objeto enrolado em um objeto de co-rotina para ser executado no loop de eventos.

Caso 1, awaitem uma co-rotina

Criamos duas corrotinas, awaituma e usamos create_taskpara executar a outra.

import asyncio
import time

# coroutine function
async def p(word):
    print(f'{time.time()} - {word}')


async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')  # coroutine
    task2 = loop.create_task(p('create_task'))  # <- runs in next iteration
    await coro  # <-- run directly
    await task2

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

você obterá o resultado:

1539486251.7055213 - await
1539486251.7055705 - create_task

Explicar:

task1 foi executado diretamente e task2 foi executado na iteração seguinte.

Caso 2, cedendo controle ao loop de eventos

Se substituirmos a função principal, podemos ver um resultado diferente:

async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')
    task2 = loop.create_task(p('create_task'))  # scheduled to next iteration
    await asyncio.sleep(1)  # loop got control, and runs task2
    await coro  # run coro
    await task2

você obterá o resultado:

-> % python coro.py
1539486378.5244057 - create_task
1539486379.5252144 - await  # note the delay

Explicar:

Ao chamar asyncio.sleep(1), o controle foi devolvido ao loop de eventos e o loop verifica se há tarefas a serem executadas e, em seguida, executa a tarefa criada por create_task.

Observe que primeiro invocamos a função de co-rotina, mas não awaitela, então apenas criamos uma única co-rotina, e não a executamos. Então, chamamos a função de corrotina novamente e a envolvemos em uma create_taskchamada, creat_task irá realmente agendar a corrotina para rodar na próxima iteração. Portanto, no resultado, create taské executado antes await.

Na verdade, o objetivo aqui é devolver o controle ao loop, você poderia usar asyncio.sleep(0)para ver o mesmo resultado.

Sob o capô

loop.create_taskrealmente chama asyncio.tasks.Task(), que chamará loop.call_soon. E loop.call_sooncolocará a tarefa em loop._ready. Durante cada iteração do loop, ele verifica cada retorno de chamada em loop._ready e o executa.

asyncio.waitasyncio.ensure_futuree , asyncio.gatherna verdade, ligar loop.create_taskdireta ou indiretamente.

Observe também na documentação :

Os retornos de chamada são chamados na ordem em que são registrados. Cada retorno de chamada será chamado exatamente uma vez.

ospider
fonte
1
Obrigado por uma explicação limpa! Tenho que dizer, é um design bastante terrível. A API de alto nível está vazando abstração de baixo nível, o que complica a API.
Boris Burkov
1
confira o projeto curio, que é bem desenhado
ospider
Bela explicação! Acho que o efeito da await task2ligação poderia ser esclarecido. Em ambos os exemplos, a chamada loop.create_task () é o que agenda task2 no loop de evento. Portanto, em ambos os exs você pode excluir o await task2e ainda o task2 eventualmente será executado. No ex2 o comportamento será idêntico, pois await task2acredito que esteja apenas agendando a tarefa já concluída (que não será executada uma segunda vez), enquanto no ex1 o comportamento será um pouco diferente, pois a tarefa2 não será executada até que o main seja concluído. Para ver a diferença, adicione print("end of main")no final do ex1 principal
André
10

Um comentário de Vincent com link para https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346 , que mostra que wait()envolve as corrotinas ensure_future()para você!

Em outras palavras, precisamos de um futuro, e as corrotinas serão silenciosamente transformadas nelas.

Atualizarei essa resposta quando encontrar uma explicação definitiva de como agrupar co-rotinas / futuros.

malha
fonte
Isso significa que para um objeto de co-rotina c, await cé equivalente a await create_task(c)?
Alexey
3

Da BDFL [2013]

Tarefas

  • É uma co-rotina envolvida em um futuro
  • a classe Task é uma subclasse da classe Future
  • Então funciona com o Wait também!

  • Como isso difere de uma co-rotina simples?
  • Pode progredir sem esperar por isso
    • Contanto que você espere por outra coisa, ou seja,
      • aguardar [alguma coisa]

Com isso em mente, ensure_futurefaz sentido como um nome para a criação de uma Tarefa, pois o resultado do Future será computado independentemente de você o esperar ou não (desde que você espere algo). Isso permite que o loop de eventos conclua sua tarefa enquanto você espera por outras coisas. Observe que no Python 3.7 create_taské a forma preferida de garantir um futuro .

Nota: Eu mudei "rendimento de" nos slides de Guido para "aguardar" aqui pela modernidade.

crizCraig
fonte