Trio - Programación asincrónica para personas

imagen

Python tiene una biblioteca Trio , una biblioteca de programación asincrónica.
Conocer a Trio será principalmente interesante para quienes trabajan en Asyncio, porque es una buena alternativa que le permite resolver algunos de los problemas que Asyncio no puede manejar. En esta revisión, consideraremos qué es Trio y qué características nos brinda.

Para aquellos que recién comienzan a trabajar en programación asincrónica, les propongo leer una pequeña introducción sobre qué son la asincronía y el sincronismo.

Sincronismo y asincronía


En la programación sincrónica, todas las operaciones se realizan de forma secuencial, y puede comenzar una nueva tarea solo después de completar la anterior. Sin embargo, uno de sus puntos de "dolor" es que si uno de los hilos ha estado trabajando en una tarea durante mucho tiempo, todo el programa puede congelarse. Hay tareas que no requieren potencia informática, pero que requieren tiempo de procesador, que pueden usarse de manera más racional al dar el control a otro subproceso. En la programación síncrona, no hay forma de pausar la tarea actual para completar lo siguiente en su espacio.

¿Para qué sirve la asincronía?? Es necesario distinguir entre asincronía verdadera y asincronía de entrada-salida. En nuestro caso, estamos hablando de entrada-salida asíncrona. A nivel mundial, para ahorrar tiempo y utilizar de manera más eficiente las instalaciones de producción. Asynchrony le permite omitir las áreas problemáticas de los hilos. En la entrada-salida asíncrona, el hilo actual no esperará la ejecución de algún evento externo, pero dará el control a otro hilo. Por lo tanto, de hecho, solo se ejecuta un hilo a la vez. El hilo que ha dado el control entra en la cola y espera a que el control regrese a él. Quizás, para ese momento, ocurrirá el evento externo esperado y será posible continuar trabajando. Esto le permitirá cambiar entre tareas para minimizar la pérdida de tiempo.

Y ahora podemos volver a lo que esAsyncio . La operación de este bucle de eventos de la biblioteca ( bucle de eventos), que incluye la cola de tareas y el bucle en sí. El ciclo controla la ejecución de tareas, es decir, extrae tareas de la cola y determina lo que le sucederá. Por ejemplo, podría estar manejando tareas de E / S. Es decir, el bucle de eventos selecciona la tarea, registra y en el momento adecuado comienza su procesamiento.

Las rutinas son funciones especiales que devuelven el control de esta tarea al bucle de eventos, es decir, las devuelven a la cola. Es necesario que estas corutinas se lancen precisamente a través de una serie de eventos.

También hay futuros- objetos en los que se almacena el resultado actual de la ejecución de una tarea. Esto puede ser información de que la tarea aún no se ha procesado o que el resultado ya se ha obtenido; o puede haber una excepción.

En general, la biblioteca Asyncio es bien conocida, sin embargo, tiene una serie de inconvenientes que Trio es capaz de cerrar.

Trío


Según el autor de la biblioteca, Nathaniel Smith , cuando desarrolló Trio, trató de crear una herramienta ligera y fácil de usar para el desarrollador, que proporcionara la entrada / salida asincrónica más simple y el manejo de errores.

Una característica importante de Trio es la gestión de contexto asíncrono, que Asyncio no tiene. Para hacer esto, el autor creó en Trio la llamada "guardería"(vivero): un área de cancelación que se responsabiliza de la atomicidad (continuidad) de un grupo de hilos. La idea clave es que si en el “vivero” falla una de las corutinas, todos los flujos en el “vivero” se completarán o cancelarán con éxito. En cualquier caso, el resultado será correcto. Y solo cuando se completan todas las rutinas, después de salir de la función, el desarrollador mismo decide cómo proceder.

Es decir, "childs" le permite evitar la continuación del procesamiento de errores, lo que puede llevar al hecho de que todo se "caerá" o que la salida será incorrecta.
Esto es exactamente lo que puede pasar con Asyncio, porque en Asyncio el proceso no se detiene, a pesar de que ocurrió un error. Y en este caso, en primer lugar, el desarrollador no sabrá qué sucedió exactamente en el momento del error y, en segundo lugar, el procesamiento continuará.

Ejemplos


Considere el ejemplo más simple de dos características en competencia:

Asyncio

import asyncio

async def foo1():
    print('  foo1: ')
    await asyncio.sleep(2)
    print('  foo1: ')

async def foo2():
    print('  foo2: ')
    await asyncio.sleep(1)
    print('  foo2: ')

loop = asyncio.get_event_loop()
bundle = asyncio.wait([
    loop.create_task(foo1()),
    loop.create_task(foo2()),
])
try:
    loop.run_until_complete(bundle)
finally:
    loop.close()

Trío

import trio

async def foo1():
    print('  foo1: ')
    await trio.sleep(2)
    print('  foo1: ')

async def foo2():
    print('  foo2: ')
    await trio.sleep(1)
    print('  foo2: ')

async def root():
    async with trio.open_nursery() as nursery:
        nursery.start_soon(foo1)
        nursery.start_soon(foo2)

trio.run(root)

en ambos casos el resultado será el mismo:

foo1: 
foo2: 
foo2: 
foo1: 

Estructuralmente, el código Asyncio y Trio en este ejemplo es similar.

La diferencia obvia es que Trio no requiere la finalización explícita del bucle de eventos.

Considere un ejemplo un poco más vivo. Hagamos una llamada al servicio web para obtener una marca de tiempo.

Para Asyncio usaremos adicionalmente aiohttp :

import time
import asyncio
import aiohttp

URL = 'https://yandex.ru/time/sync.json?geo=213'
MAX_CLIENTS = 5

async def foo(session, i):
    start = time.time()
    async with session.get(URL) as response:
        content = await response.json()
        print(f'{i} | {content.get("time")} (  {time.time() - start})')

async def root():
    start = time.time()
    async with aiohttp.ClientSession() as session:
        tasks = [
            asyncio.ensure_future(foo(session, i))
            for i in range(MAX_CLIENTS)
        ]
        await asyncio.wait(tasks)
    print(f'  {time.time() - start}')

ioloop = asyncio.get_event_loop()
try:
    ioloop.run_until_complete(root())
finally:
    ioloop.close()

Para Trio usamos preguntas :

import trio
import time
import asks
URL = 'https://yandex.ru/time/sync.json?geo=213'
MAX_CLIENTS = 5

asks.init('trio')

async def foo(i):
    start = time.time()
    response = await asks.get(URL)
    content = response.json()
    print(f'{i} | {content.get("time")} (  {time.time() - start})')

async def root():
    start = time.time()
    async with trio.open_nursery() as nursery:
        for i in range(MAX_CLIENTS):
            nursery.start_soon(foo, i)

    print(f'  {time.time() - start}')

trio.run(root)

En ambos casos, obtenemos algo como

0 | 1543837647522 (  0.11855053901672363)
2 | 1543837647535 (  0.1389765739440918)
3 | 1543837647527 (  0.13904547691345215)
4 | 1543837647557 (  0.1591191291809082)
1 | 1543837647607 (  0.2100353240966797)
  0.2102828025817871

Bueno. Imagine que durante la ejecución de una de las corutinas, ocurrió un error
para Asyncio.

async def foo(session, i):
    start = time.time()
    if i == 3:
        raise Exception
    async with session.get(URL) as response:
        content = await response.json()
        print(f'{i} | {content.get("time")} (  {time.time() - start})')

1 | 1543839060815 (  0.10857725143432617)
2 | 1543839060844 (  0.10372781753540039)
5 | 1543839060843 (  0.10734415054321289)
4 | 1543839060874 (  0.13985681533813477)
  0.15044045448303223
Traceback (most recent call last):
  File "...py", line 12, in foo
    raise Exception
Exception

para trio

async def foo(i):
    start = time.time()
    response = await asks.get(URL)
    content = response.json()
    if i == 3:
        raise Exception
    print(f'{i} | {content.get("time")} (  {time.time() - start})')


4 | 1543839223372 (  0.13524699211120605)
2 | 1543839223379 (  0.13848185539245605)
Traceback (most recent call last):
  File "...py", line 28, in <module>
    trio.run(root)
  File "/lib64/python3.6/site-packages/trio/_core/_run.py", line 1337, in run
    raise runner.main_task_outcome.error
  File "...py", line 23, in root
    nursery.start_soon(foo, i)
  File "/lib64/python3.6/site-packages/trio/_core/_run.py", line 397, in __aexit__
    raise combined_error_from_nursery
  File "...py", line 15, in foo
    raise Exception
Exception

Se ve claramente que en Trio, inmediatamente después de la ocurrencia del error, el "área de cancelación" funcionó, y dos de las cuatro tareas que no contenían errores se terminaron anormalmente.

En Asyncio, todas las tareas se completaron, y solo entonces apareció el trackback.

En el ejemplo dado, esto no es importante, pero imaginemos que las tareas de una forma u otra dependen unas de otras, y el conjunto de tareas debe tener la propiedad de atomicidad. En este caso, la respuesta oportuna a un error se vuelve mucho más importante. Por supuesto, puede usar await asyncio.wait (tareas, return_when = FIRST_EXCEPTION) , pero debe recordar completar correctamente las tareas abiertas.

Aquí hay otro ejemplo:

suponga que las rutinas acceden simultáneamente a varios servicios web similares, y la primera respuesta recibida es importante.

import asyncio
from asyncio import FIRST_COMPLETED
import aiohttp

URL = 'https://yandex.ru/time/sync.json?geo=213'
MAX_CLIENTS = 5

async def foo(session):
    async with session.get(URL) as response:
        content = await response.json()
        return content.get("time")

async def root():
    async with aiohttp.ClientSession() as session:
        tasks = [
            asyncio.ensure_future(foo(session))
            for i in range(1, MAX_CLIENTS + 1)
        ]
        done, pending = await asyncio.wait(tasks, return_when=FIRST_COMPLETED)
        print(done.pop().result())
        for future in pending:
            future.cancel()

ioloop = asyncio.get_event_loop()
try:
    ioloop.run_until_complete(root())
except:
    ioloop.close()

Todo es bastante simple. El único requisito es recordar completar las tareas que no se han completado.

En Trio, hacer una maniobra similar es un poco más difícil, pero es casi imposible dejar las "colas" invisibles de inmediato:

import trio
import asks
URL = 'https://yandex.ru/time/sync.json?geo=213'
MAX_CLIENTS = 5
asks.init('trio')

async def foo(session, send_channel, nursery):
    response = await session.request('GET', url=URL)
    content = response.json()
    async with send_channel:
        send_channel.send_nowait(content.get("time"))
    nursery.cancel_scope.cancel()

async def root():
    send_channel, receive_channel = trio.open_memory_channel(1)
    async with send_channel, receive_channel:
        async with trio.open_nursery() as nursery:
            async with asks.Session() as session:
                for i in range(MAX_CLIENTS):
                    nursery.start_soon(foo, session, send_channel.clone(), nursery)

        async with receive_channel:
            x = await receive_channel.receive()
            print(x)

trio.run(root)

nursery.cancel_scope.cancel () : la primera rutina que se completa llamará a una función en el área de deshacer que cancelará todas las demás tareas, por lo que no hay necesidad de preocuparse por separado.
Es cierto que para transferir el resultado de la ejecución de rutina a la función que lo causó, deberá iniciar un canal de comunicación.

Esperemos que esta revisión comparativa haya proporcionado una comprensión de las características principales de Trio. ¡Gracias a todos!

All Articles