¿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:
- http://masnun.com/2015/11/13/python-generators-coroutines-native-coroutines-and-async-await.html
- http://masnun.com/2015/11/20/python-asyncio-future-task-and-the-event-loop.html
- http://masnun.com/2015/12/07/python-3-using-blocking-functions-or-codes-with-asyncio.html
Por supuesto, puede encontrar más detalles en el manual oficial: https://docs.python.org/3/library/asyncio.html
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]
-
Mientras esperes por algo más, es decir
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.