python python-asyncio python-3.5

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



python-asyncio python-3.5 (4)

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:

coros = [] for i in range(5): coros.append(foo(i)) loop = get_event_loop() loop.run_until_complete(wait(coros))

Y:

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

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

Independientemente del valor de retorno, estoy buscando claridad en ensure_future() . wait(coros) y wait(futures) ejecutan las corutinas, entonces, ¿cuándo y por qué debe envolverse una corutina en ensure_future ?

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

Para obtener crédito adicional, ¿qué sucede si deseo agrupar las llamadas? Por ejemplo, necesito llamar a some_remote_call(...) 1000 veces, pero no quiero destruir el servidor web / base de datos / etc con 1000 conexiones simultáneas. Esto se puede hacer con un subproceso o grupo de procesos, pero ¿hay alguna manera de hacerlo con asyncio ?


Respuesta simple

  • Invocar una función de rutina ( async def ) NO la ejecuta. Devuelve un objeto de rutina, como la función de generador devuelve objetos de generador.
  • await recupera valores de las rutinas, es decir, "llama" a la rutina
  • eusure_future/create_task programa la ejecución de la rutina 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 async def s;
  • objeto de rutina, lo que obtienes cuando "llamas" a una función de rutina;
  • tarea, un objeto envuelto alrededor de un objeto de rutina para ejecutarse en el bucle de eventos.

Caso 1, await en una rutina

Creamos dos corutinas, await una y usamos create_task para 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á 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á resultado:

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

Explique:

Al llamar a asyncio.sleep(1) , el control regresó al bucle de eventos, y el bucle comprueba si hay tareas que ejecutar, luego ejecuta la tarea creada por create_task .

Tenga en cuenta que, primero invocamos la función de rutina, pero no la await , por lo que solo creamos una sola y no la ejecutamos. Luego, llamamos nuevamente a la función de rutina y la envolvemos en una llamada create_task, creat_task en realidad programará que la corutina se ejecute en la próxima iteración. Entonces, en el resultado, la create task se ejecuta antes de 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_task realidad llama a asyncio.tasks.Task() , que llamará a loop.call_soon . Y loop.call_soon pondrá la tarea en loop._ready . Durante cada iteración del bucle, comprueba todas las devoluciones de llamada en loop._ready y lo ejecuta.

asyncio.wait , asyncio.ensure_future y asyncio.gather realmente llaman a loop.create_task directa o indirectamente.

También tenga en cuenta en los docs :

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


¡Un comentario de Vincent vinculado a https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346 , que muestra que wait() envuelve las corutinas en ensure_future() para usted!

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

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


Una corutina es una función generadora que puede generar valores y aceptar valores del exterior. El beneficio de usar una rutina 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 otras funciones.

Un futuro es como los objetos Promise de Javascript. Es como un marcador de posición para un valor que se materializará en el futuro. En el caso mencionado anteriormente, mientras se espera en la E / S de la red, una función puede proporcionarnos 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 necesita ensure_future si no necesita los resultados. Son buenos si necesita los resultados o recuperar excepciones ocurridas.

Créditos adicionales: elegiría run_in_executor y pasaría una instancia de Executor para controlar la cantidad máxima de trabajadores.

Explicaciones y códigos de muestra

En el primer ejemplo, está utilizando corutinas. La función de wait toma un montón de corutinas y las combina. Así que wait() finaliza cuando se agotan todas las corutinas (completadas / terminadas, devolviendo todos los valores).

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

El método run_until_complete se aseguraría de que el ciclo esté activo hasta que finalice la ejecución. Observe cómo no está obteniendo los resultados de la ejecución asíncrona en este caso.

En el segundo ejemplo, está utilizando la función ensure_future para ajustar una rutina y devolver un objeto Task que es una especie de Future . La corutina está programada para ejecutarse en el bucle principal de eventos cuando llama a ensure_future . El objeto futuro / tarea devuelto aún no tiene un valor, pero con el tiempo, cuando finalicen las operaciones de red, el objeto futuro mantendrá 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 corutinas.

Veamos un ejemplo de cómo usar asyncio / coroutines / 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()

Aquí, hemos usado el método create_task en el objeto de loop . ensure_future programaría la tarea en el bucle principal del evento. Este método nos permite programar una rutina en un bucle que elegimos.

También vemos el concepto de agregar una devolución de llamada utilizando el método add_done_callback en el objeto de tarea.

Una Task se done cuando la rutina devuelve un valor, genera una excepción o se cancela. Existen métodos para verificar 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


De la BDFL [2013]

Tareas

  • Es una corutina envuelta en un futuro
  • clase Task es una subclase de clase Future
  • Por lo tanto, funciona con esperar también!
  • ¿En qué se diferencia de una corutina desnuda?
  • Puede progresar sin esperarlo
    • Mientras esperes por algo más, es decir
      • espera [algo_else]

Con esto en mente, ensure_future tiene sentido como un nombre para crear una Tarea ya que el resultado del Futuro se calculará si lo espera o no (siempre que espere algo). Esto permite que el bucle de eventos complete su Tarea mientras espera otras cosas. Tenga en cuenta que en Python 3.7 create_task es la forma preferida de garantizar un futuro .

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