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:Asyncioimport 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()
Trioimport 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 como0 | 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 erropara 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 trioasync 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!