Multitasking cooperativo con co-rutinas

Las co-rutinas son una construcción de lenguaje diseñada para operaciones concurrentes. Una función de co-rutina crea un objeto de co-rutina cuando es llamada, y la persona que llama puede ejecutar el código de la función usando el el método send() de co-rutina. Una co-rutina puede pausar la ejecución usando la palabra clave await con otra co-rutina. Mientras está en pausa, el estado de la co-rutina se mantiene, lo que le permite reanudarse donde se dejó apagada la próxima vez que se despierta.

Comenzar una co-rutina

Hay formas diferentes de tener que el bucle de eventos asyncio inicie una co-rutina. La más simple es usar run_until_complete(), pasándole la co-rutina directamente.

asyncio_coroutine.py
import asyncio


async def coroutine():
    print('in coroutine')


event_loop = asyncio.get_event_loop()
try:
    print('starting coroutine')
    coro = coroutine()
    print('entering event loop')
    event_loop.run_until_complete(coro)
finally:
    print('closing event loop')
    event_loop.close()

El primer paso es obtener una referencia al bucle de eventos. El tipo de bucle por defecto se puede usar, o una clase de bucle específica puede ser instanciada. En este ejemplo, se utiliza el bucle predeterminado. El método run_until_complete() inicia el bucle con el objeto co-rutina y detiene el bucle cuando la co-rutina sale al retornar.

$ python3 asyncio_coroutine.py

starting coroutine
entering event loop
in coroutine
closing event loop

Valores de retorno de las co-rutinas

El valor de retorno de una co-rutina se devuelve al código que la comienza y la espera.

asyncio_coroutine_return.py
import asyncio


async def coroutine():
    print('in coroutine')
    return 'result'


event_loop = asyncio.get_event_loop()
try:
    return_value = event_loop.run_until_complete(
        coroutine()
    )
    print('it returned: {!r}'.format(return_value))
finally:
    event_loop.close()

En este caso, run_until_complete() también devuelve el resultado de la co-rutina que está esperando.

$ python3 asyncio_coroutine_return.py

in coroutine
it returned: 'result'

Encadenar co-rutinas

Una co-rutina puede comenzar otra co-rutina y esperar los resultados. Esto facilita la descomposición de una tarea en partes reutilizables. El siguiente ejemplo tiene dos fases que deben ejecutarse en orden, pero pueden ejecutarse simultáneamente con otras operaciones.

asyncio_coroutine_chain.py
import asyncio


async def outer():
    print('in outer')
    print('waiting for result1')
    result1 = await phase1()
    print('waiting for result2')
    result2 = await phase2(result1)
    return (result1, result2)


async def phase1():
    print('in phase1')
    return 'result1'


async def phase2(arg):
    print('in phase2')
    return 'result2 derived from {}'.format(arg)


event_loop = asyncio.get_event_loop()
try:
    return_value = event_loop.run_until_complete(outer())
    print('return value: {!r}'.format(return_value))
finally:
    event_loop.close()

La palabra clave await se usa en lugar de agregar las nuevas co-rutinas al bucle, porque el flujo de control ya está dentro de una co-rutina gestionada por el bucle, por lo que no es necesario indicarle al bucle que gestione las nuevas co-rutinas.

$ python3 asyncio_coroutine_chain.py

in outer
waiting for result1
in phase1
waiting for result2
in phase2
return value: ('result1', 'result2 derived from result1')

Generadores en lugar de co-rutinas

Las funciones de co-rutina son un componente clave del diseño de asyncio. Proporcionan un lenguaje de construcción para detener la ejecución de parte de un programa, preservando el estado de esa ejecución, y volver a entrar en el estado en un momento posterior, que son todas capacidades importantes para un marco de concurrencia.

Python 3.5 introdujo nuevas características de lenguaje para definir tales co-rutinas de forma nativa usando async def y ceder el control usando await, y los ejemplos para asyncio aprovechan la nueva característica. Las versiones anteriores de Python 3 pueden usar funciones de generador envueltas con el decorador asyncio.coroutine() y yield from para lograr el mismo efecto.

asyncio_generator.py
import asyncio


@asyncio.coroutine
def outer():
    print('in outer')
    print('waiting for result1')
    result1 = yield from phase1()
    print('waiting for result2')
    result2 = yield from phase2(result1)
    return (result1, result2)


@asyncio.coroutine
def phase1():
    print('in phase1')
    return 'result1'


@asyncio.coroutine
def phase2(arg):
    print('in phase2')
    return 'result2 derived from {}'.format(arg)


event_loop = asyncio.get_event_loop()
try:
    return_value = event_loop.run_until_complete(outer())
    print('return value: {!r}'.format(return_value))
finally:
    event_loop.close()

El ejemplo anterior reproduce asyncio_coroutine_chain.py usando funciones generadoras en lugar de co-rutinas nativos.

$ python3 asyncio_generator.py

in outer
waiting for result1
in phase1
waiting for result2
in phase2
return value: ('result1', 'result2 derived from result1')