Rendimiento de coroutine vs rendimiento de tarea

Guido van Rossum, en su discurso en 2014 en Tulip / Asyncio muestra la diapositiva :

Tareas vs coroutines

  • Comparar:

    • res = rendimiento de some_coroutine (…)
    • res = rendimiento de la tarea (some_coroutine (…))
  • La tarea puede avanzar sin esperarla.

    • Como registro mientras esperas algo más.
      • es decir, el rendimiento de

Y estoy perdiendo completamente el punto.

Desde mi punto de vista, ambas construcciones son idénticas:

En el caso de coroutine desnudo: se progtwig, por lo que la tarea se crea de todos modos, ya que el progtwigdor opera con Tareas, luego el coroutine de la persona que llama se suspende hasta que el callee finaliza y queda libre para continuar la ejecución.

En el caso de Task – Todas igual – la nueva tarea está progtwigda y la persona que realiza la llamada espera su finalización.

¿Cuál es la diferencia en la forma en que se ejecutó el código en ambos casos y qué impacto tiene que el desarrollador debe considerar en la práctica?

PD
Los enlaces a las fonts autorizadas (GvR, PEP, documentos, notas de los desarrolladores) serán muy apreciados.

Para el lado que llama, el yield from coroutine() co-rutinario yield from coroutine() siente como una llamada de función (es decir, volverá a ganar control cuando termine coroutine).

yield from Task(coroutine()) por otro lado se siente más como crear un nuevo hilo. Task() regresa casi al instante y es muy probable que la persona que llama recupere el control antes de que coroutine() la coroutine() .

La diferencia entre f() y th = threading.Thread(target=f, args=()); th.start(); th.join() th = threading.Thread(target=f, args=()); th.start(); th.join() th = threading.Thread(target=f, args=()); th.start(); th.join() es obvio, ¿verdad?

El punto de usar asyncio.Task(coro()) es para los casos en los que no desea esperar explícitamente a coro , pero desea que se ejecute en segundo plano mientras espera otras tareas. Eso es lo que significa la diapositiva de Guido por

[A] La Task puede avanzar sin esperarla … mientras espere otra cosa

Considera este ejemplo:

 import asyncio @asyncio.coroutine def test1(): print("in test1") @asyncio.coroutine def dummy(): yield from asyncio.sleep(1) print("dummy ran") @asyncio.coroutine def main(): test1() yield from dummy() loop = asyncio.get_event_loop() loop.run_until_complete(main()) 

Salida:

 dummy ran 

Como puede ver, la test1 nunca se ejecutó realmente, porque no llamamos explícitamente el yield from ella.

Ahora, si usamos asyncio.async para envolver una instancia de Task alrededor de test1 , el resultado es diferente:

 import asyncio @asyncio.coroutine def test1(): print("in test1") @asyncio.coroutine def dummy(): yield from asyncio.sleep(1) print("dummy ran") @asyncio.coroutine def main(): asyncio.async(test1()) yield from dummy() loop = asyncio.get_event_loop() loop.run_until_complete(main()) 

Salida:

 in test1 dummy ran 

Entonces, realmente no hay una razón práctica para usar el yield from asyncio.async(coro()) , ya que es más lento que el yield from coro() sin ningún beneficio; introduce la sobrecarga de agregar coro al progtwigdor interno de asyncio , pero eso no es necesario, ya que el uso yield from garantías de que coro se ejecutará, de todos modos. Si solo desea llamar a una coroutine y esperar a que termine, solo cámbielo directamente.

Nota al margen:

Estoy usando asyncio.async * en lugar de Task directamente porque la documentación lo recomienda :

No cree directamente instancias de Task : use la función async() o el método BaseEventLoop.create_task() .

* Tenga en cuenta que a partir de Python 3.4.4, asyncio.async está en desuso en favor de asyncio.ensure_future .

Como se describe en PEP 380, el documento PEP aceptado que introdujo el rendimiento de, la expresión res = yield from f() proviene de la idea del siguiente bucle:

 for res in f(): yield res 

Con esto, las cosas se vuelven muy claras: si f() es some_coroutine() , entonces se ejecuta la coroutine. Por otro lado, si f() es Task(some_coroutine()) , Task.__init__ se ejecuta en su lugar. some_coroutine() no se ejecuta, solo el generador recién creado se pasa como primer argumento a la Task.__init__ .

Conclusión:

  • res = yield from some_coroutine() => coroutine continúa la ejecución y devuelve el siguiente valor
  • res = yield from Task(some_coroutine()) => se crea una nueva tarea, que almacena un objeto generador de some_coroutine() no ejecutado.