Trio - Programação assíncrona para pessoas

imagem

O Python possui uma biblioteca Trio - uma biblioteca de programação assíncrona.
Conhecer o Trio será principalmente interessante para quem trabalha com o Asyncio, porque é uma boa alternativa que permite resolver alguns dos problemas que o Asyncio não consegue resolver. Nesta revisão, consideraremos o que é o Trio e quais recursos ele oferece.

Para aqueles que estão apenas começando a trabalhar em programação assíncrona, proponho ler uma pequena introdução sobre o que são assincronia e sincronismo.

Sincronismo e assincronia


Na programação síncrona, todas as operações são executadas seqüencialmente, e você pode iniciar uma nova tarefa somente após concluir a anterior. No entanto, um dos pontos “problemáticos” é que, se um dos threads estiver trabalhando em uma tarefa há muito tempo, o programa inteiro poderá congelar. Existem tarefas que não exigem capacidade de computação, mas ocupam o tempo do processador, que pode ser usado de maneira mais racional, dando controle a outro encadeamento. Na programação síncrona, não há como pausar a tarefa atual para concluir o seguinte em seu espaço.

Para que serve a assincronia?? É necessário distinguir entre assincronia verdadeira e assincronia de entrada e saída. No nosso caso, estamos falando de entrada e saída assíncrona. Globalmente - para economizar tempo e usar de maneira mais eficiente as instalações de produção. A assincronia permite ignorar as áreas problemáticas dos threads. Na entrada-saída assíncrona, o encadeamento atual não aguardará a execução de algum evento externo, mas dará controle para outro encadeamento. Assim, de fato, apenas um encadeamento é executado por vez. O encadeamento que forneceu o controle entra na fila e aguarda o retorno do controle. Talvez a essa altura o evento externo esperado ocorra e seja possível continuar trabalhando. Isso permitirá que você alterne entre tarefas para minimizar a perda de tempo.

E agora podemos voltar ao que éAsyncio . A operação deste loop de eventos da biblioteca ( loop de eventos), que inclui a fila de tarefas e o próprio loop. O ciclo controla a execução de tarefas, ou seja, extrai tarefas da fila e determina o que acontecerá com ela. Por exemplo, pode estar lidando com tarefas de E / S. Ou seja, o loop de eventos seleciona a tarefa, registra e no momento certo inicia seu processamento.

As corotinas são funções especiais que retornam o controle dessa tarefa ao loop de eventos, ou seja, retornam à fila. É necessário que essas corotinas sejam lançadas precisamente através de uma série de eventos.

Também há futuros- objetos nos quais o resultado atual da execução de uma tarefa está armazenado. Pode ser que a tarefa ainda não tenha sido processada ou que o resultado já tenha sido obtido; ou pode haver uma exceção.

Em geral, a biblioteca Asyncio é bem conhecida, no entanto, possui várias desvantagens que o Trio é capaz de fechar.

Trio


De acordo com o autor da biblioteca, Nathaniel Smith , ao desenvolver o Trio, ele procurou criar uma ferramenta leve e fácil de usar para o desenvolvedor, que forneceria a entrada / saída assíncrona mais simples e o tratamento de erros.

Um recurso importante do Trio é o gerenciamento de contexto assíncrono, que o Asyncio não possui. Para isso, o autor criou no Trio o chamado "berçário"(berçário) - uma área de cancelamento que assume a responsabilidade pela atomicidade (continuidade) de um grupo de threads. A idéia principal é que, se no “berçário” uma das corotinas falhar, todos os fluxos no “berçário” serão concluídos ou cancelados com sucesso. De qualquer forma, o resultado estará correto. E somente quando todas as corotinas são concluídas, após sair da função, o próprio desenvolvedor decide como proceder.

Ou seja, "filhos" permite impedir a continuação do processamento de erros, o que pode levar ao fato de que tudo "cairá" ou o resultado será um resultado incorreto.
É exatamente o que pode acontecer com o Asyncio, porque no Asyncio o processo não para, apesar do erro. E, nesse caso, em primeiro lugar, o desenvolvedor não saberá exatamente o que aconteceu no momento do erro e, em segundo lugar, o processamento continuará.

Exemplos


Considere o exemplo mais simples de dois recursos concorrentes:

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

Trio

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)

nos dois casos, o resultado será o mesmo:

foo1: 
foo2: 
foo2: 
foo1: 

Estruturalmente, o código Asyncio e Trio neste exemplo é semelhante.

A diferença óbvia é que o Trio não exige a conclusão explícita do loop de eventos.

Considere um exemplo um pouco mais animado. Vamos fazer uma ligação para o serviço web para obter um carimbo de data / hora.

Para o Asyncio, usaremos adicionalmente o 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 o Trio, usamos perguntas :

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)

Nos dois casos, temos algo como

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

Boa. Imagine que durante a execução de uma das corotinas, ocorreu um erro
para o 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

Vê-se claramente que no Trio, imediatamente após a ocorrência do erro, a “área de cancelamento” funcionou e duas das quatro tarefas que não continham erros foram encerradas de forma anormal.

No Asyncio, todas as tarefas foram concluídas e somente então o trackback apareceu.

No exemplo dado, isso não é importante, mas vamos imaginar que as tarefas de uma maneira ou de outra dependam uma da outra, e o conjunto de tarefas deve ter a propriedade de atomicidade. Nesse caso, a resposta oportuna a um erro se torna muito mais importante. Obviamente, você pode usar waitit asyncio.wait (tarefas, return_when = FIRST_EXCEPTION) , mas você deve se lembrar de concluir corretamente as tarefas abertas.

Aqui está outro exemplo:

suponha que as corotinas acessem simultaneamente vários serviços da Web semelhantes e a primeira resposta recebida seja 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()

Tudo é bem simples. O único requisito é lembrar de concluir tarefas que não foram concluídas.

No Trio, iniciar uma manobra semelhante é um pouco mais difícil, mas é quase impossível deixar as “caudas” invisíveis imediatamente:

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 () - a primeira corotina concluída chamará uma função na área de desfazer que cancelará todas as outras tarefas, portanto, não será necessário se preocupar com isso separadamente.
É verdade que, para transferir o resultado da execução da rotina para a função que a causou, você precisará iniciar um canal de comunicação.

Esperamos que esta revisão comparativa tenha fornecido um entendimento das principais características do Trio. Obrigado a todos!

All Articles