¿Diferencia entre corrutina y futuro / tarea en Python 3.5?

100

Digamos que tenemos una función ficticia:

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

Cuál es la diferencia entre:

import asyncio    

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

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

Y:

import asyncio

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

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

Nota : El ejemplo devuelve un resultado, pero este no es el foco de la pregunta. Cuando el valor de retorno importa, use en gather()lugar de wait().

Independientemente del valor de retorno, busco claridad ensure_future(). wait(coros)y wait(futures)ambos ejecutan las corrutinas, entonces, ¿cuándo y por qué debería incluirse una corrutina ensure_future?

Básicamente, ¿cuál es la forma correcta (tm) de ejecutar un montón de operaciones sin bloqueo usando Python 3.5 async?

Para obtener crédito adicional, ¿qué sucede si quiero agrupar las llamadas? Por ejemplo, necesito llamar some_remote_call(...)1000 veces, pero no quiero aplastar el servidor web / base de datos / etc.con 1000 conexiones simultáneas. Esto es factible con un hilo o grupo de procesos, pero ¿hay alguna forma de hacerlo asyncio?

Actualización 2020 (Python 3.7+) : no use estos fragmentos. En su lugar use:

import asyncio

async def do_something_async():
    tasks = []
    for i in range(5):
        tasks.append(asyncio.create_task(foo(i)))
    await asyncio.gather(*tasks)

def do_something():
    asyncio.run(do_something_async)

También considere usar Trio , una alternativa sólida de terceros a asyncio.

knite
fuente

Respuestas:

95

Una corrutina es una función generadora que puede generar valores y aceptar valores del exterior. El beneficio de usar una corrutina es que podemos pausar la ejecución de una función y reanudarla más tarde. En el caso de una operación de red, tiene sentido pausar la ejecución de una función mientras esperamos la respuesta. Podemos usar el tiempo para ejecutar algunas otras funciones.

Un futuro es como los Promiseobjetos de Javascript. Es como un marcador de posición para un valor que se materializará en el futuro. En el caso mencionado anteriormente, mientras esperamos en la E / S de la red, una función puede darnos un contenedor, una promesa de que llenará el contenedor con el valor cuando se complete la operación. Nos aferramos al objeto futuro y cuando se cumple, podemos llamar a un método para recuperar el resultado real.

Respuesta directa: no es necesario ensure_futuresi no necesita los resultados. Son buenos si necesita los resultados o recuperar las excepciones ocurridas.

Créditos adicionales: elegiría run_in_executory pasaría una Executorinstancia para controlar el número máximo de trabajadores.

Explicaciones y códigos de muestra

En el primer ejemplo, está utilizando corrutinas. La waitfunción toma un montón de corrutinas y las combina. Entonces wait()termina cuando se agotan todas las corrutinas (completadas / terminadas de devolver todos los valores)

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

El run_until_completemétodo se aseguraría de que el bucle esté vivo hasta que finalice la ejecución. Observe cómo no obtiene los resultados de la ejecución asíncrona en este caso.

En el segundo ejemplo, está utilizando la ensure_futurefunción para ajustar una corrutina y devolver un Taskobjeto que es una especie de Future. La corrutina está programada para ejecutarse en el bucle de eventos principal cuando llame ensure_future. El objeto futuro / tarea devuelto aún no tiene un valor, pero con el tiempo, cuando finalizan las operaciones de red, el objeto futuro contendrá el resultado de la operación.

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

Entonces, en este ejemplo, estamos haciendo lo mismo excepto que estamos usando futuros en lugar de solo usar corrutinas.

Veamos un ejemplo de cómo usar asyncio / coroutines / futuros:

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

Aquí, hemos utilizado el create_taskmétodo en el loopobjeto. ensure_futureprogramaría la tarea en el bucle de eventos principal. Este método nos permite programar una corrutina en un ciclo que elijamos.

También vemos el concepto de agregar una devolución de llamada usando el add_done_callbackmétodo en el objeto de tarea.

A Taskes donecuando la corrutina devuelve un valor, genera una excepción o se cancela. Existen métodos para comprobar estos incidentes.

He escrito algunas publicaciones de blog sobre estos temas que podrían ayudar:

Por supuesto, puede encontrar más detalles en el manual oficial: https://docs.python.org/3/library/asyncio.html

masnun
fuente
3
Actualicé mi pregunta para que sea un poco más clara: si no necesito el resultado de la corrutina, ¿debo seguir usando ensure_future()? Y si necesito el resultado, ¿no puedo usarlo run_until_complete(gather(coros))?
knite
1
ensure_futureprograma la corrutina que se ejecutará en el bucle de eventos. Entonces yo diría que sí, es obligatorio. Pero, por supuesto, también puede programar las corrutinas utilizando otras funciones / métodos. Sí, puede usar gather(), pero recopilar esperará hasta que se recopilen todas las respuestas.
masnun
5
@AbuAshrafMasnun @knite gathery waitenvuelva las corrutinas dadas como tareas usando ensure_future(vea las fuentes aquí y aquí ). Por lo tanto, no tiene sentido usarlo de ensure_futureantemano, y no tiene nada que ver con obtener los resultados o no.
Vincent
8
@AbuAshrafMasnun @knite Además, ensure_futuretiene un loopargumento, por lo que no hay razón para usar loop.create_taskover ensure_future. Y run_in_executorno funcionará con corrutinas, se debe usar un semáforo en su lugar.
Vincent
2
@vincent hay una razón para usar create_taskover ensure_future, vea los documentos . Citacreate_task() (added in Python 3.7) is the preferable way for spawning new tasks.
masi
24

Respuesta simple

  • La invocación de una función de rutina ( async def) NO la ejecuta. Devuelve objetos de una corrutina, como la función generadora devuelve objetos generadores.
  • await recupera valores de corrutinas, es decir, "llama" a la corrutina
  • eusure_future/create_task programe la corrutina para que se ejecute en el bucle de eventos en la próxima iteración (aunque sin esperar a que finalicen, como un hilo de demonio).

Algunos ejemplos de código

Primero aclaremos algunos términos:

  • función de rutina, la que tienes async def;
  • objeto coroutine, lo que obtienes cuando "llamas" a una función coroutine;
  • tarea, un objeto envuelto alrededor de un objeto de rutina para ejecutar en el bucle de eventos.

Caso 1, awaiten una corrutina

Creamos dos corrutinas, awaituna, y usamos create_taskpara ejecutar la otra.

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

obtendrás resultado:

1539486251.7055213 - await
1539486251.7055705 - create_task

Explique:

task1 se ejecutó directamente y task2 se ejecutó en la siguiente iteración.

Caso 2, ceder el control al bucle de eventos

Si reemplazamos la función principal, podemos ver un 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

obtendrás resultado:

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

Explique:

Al llamar asyncio.sleep(1), el control se devolvió al bucle de eventos, y el bucle verifica que las tareas se ejecuten y luego ejecuta la tarea creada por create_task.

Tenga en cuenta que primero invocamos la función de corrutina, pero no a awaitella, por lo que solo creamos una sola corrutina y no la ejecutamos. Luego, volvemos a llamar a la función de rutina y la envolvemos en una create_taskllamada, creat_task programará la rutina para que se ejecute en la siguiente iteración. Entonces, en el resultado, create taskse ejecuta antes await.

En realidad, el punto aquí es devolver el control al bucle, podría usar asyncio.sleep(0)para ver el mismo resultado.

Bajo el capó

loop.create_tasken realidad llama asyncio.tasks.Task(), que llamará loop.call_soon. Y loop.call_soonpondrá la tarea en loop._ready. Durante cada iteración del ciclo, comprueba todas las devoluciones de llamada en loop._ready y lo ejecuta.

asyncio.wait, asyncio.ensure_futurey de asyncio.gatherhecho llamar loop.create_taskdirecta o indirectamente.

También tenga en cuenta en los documentos :

Las devoluciones de llamada se llaman en el orden en que se registran. Cada devolución de llamada se llamará exactamente una vez.

ospider
fuente
1
¡Gracias por una explicación clara! Tengo que decir que es un diseño bastante terrible. La API de alto nivel tiene fugas de abstracción de bajo nivel, lo que complica demasiado la API.
Boris Burkov
1
echa un vistazo al proyecto de curiosidades, que está bien diseñado
ospider
¡Buena explicación! Creo que se await task2podría aclarar el efecto de la llamada. En ambos ejemplos, la llamada loop.create_task () es lo que programa task2 en el ciclo de eventos. Entonces, en ambos exs, puede eliminar el await task2y aún así task2 eventualmente se ejecutará. En ex2, el comportamiento será idéntico, ya await task2que creo que solo está programando la tarea ya completada (que no se ejecutará por segunda vez), mientras que en ex1 el comportamiento será ligeramente diferente ya que la tarea2 no se ejecutará hasta que la principal esté completa. Para ver la diferencia, agregue print("end of main")al final de la página principal de ex1
Andrew
10

Un comentario de Vincent vinculado a https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346 , que muestra que te wait()envuelve las corrutinas ensure_future().

En otras palabras, necesitamos un futuro, y las corrutinas se transformarán silenciosamente en ellos.

Actualizaré esta respuesta cuando encuentre una explicación definitiva de cómo agrupar corutinas / futuros.

knite
fuente
¿Significa que para un objeto de rutina c, await ces equivalente a await create_task(c)?
Alexey
3

Del BDFL [2013]

Tareas

  • Es una corrutina envuelta en un futuro
  • class Task es una subclase de la clase Future
  • ¡Así que también funciona con await !

  • ¿En qué se diferencia de una corrutina simple?
  • Puede progresar sin esperarlo
    • Siempre que espere algo más, es decir
      • espera [algo_más]

Con esto en mente, ensure_futuretiene sentido como un nombre para la creación de un Grupo a que el resultado de que el futuro será computada si está o no esperan que (siempre y cuando usted espera algo). Esto permite que el ciclo de eventos complete su tarea mientras espera otras cosas. Tenga en cuenta que en Python 3.7 create_taskes la forma preferida de asegurar un futuro .

Nota: Cambié "rendimiento de" en las diapositivas de Guido a "esperar" aquí la modernidad.

crizCraig
fuente