Essa pergunta é motivada por outra pergunta: Como esperar no cdef?
Existem toneladas de artigos e postagens de blogs na web sobre asyncio
, mas todos são muito superficiais. Não consegui encontrar nenhuma informação sobre como asyncio
realmente é implementado e o que torna o I / O assíncrono. Eu estava tentando ler o código-fonte, mas são milhares de linhas de código C não do mais alto grau, muitas das quais lidam com objetos auxiliares, mas o mais crucial, é difícil conectar entre a sintaxe Python e o código C que ela traduziria para dentro.
A documentação do próprio Asycnio é ainda menos útil. Não há nenhuma informação sobre como funciona, apenas algumas orientações sobre como usá-lo, que às vezes também são enganosas / mal escritas.
Estou familiarizado com a implementação de corrotinas do Go e esperava que o Python fizesse a mesma coisa. Se fosse esse o caso, o código que sugeri no post com link acima teria funcionado. Como isso não aconteceu, agora estou tentando descobrir o porquê. Meu melhor palpite até agora é o seguinte, corrija-me onde estou errado:
- As definições de procedimento do formulário
async def foo(): ...
são, na verdade, interpretadas como métodos de herança de uma classecoroutine
. - Talvez,
async def
na verdade , seja dividido em vários métodos porawait
instruções, onde o objeto, no qual esses métodos são chamados, é capaz de acompanhar o progresso feito durante a execução até o momento. - Se o acima for verdadeiro, então, essencialmente, a execução de uma co-rotina se resume a chamar métodos de objeto de co-rotina por algum gerenciador global (loop?).
- O gerenciador global está de alguma forma (como?) Ciente de quando as operações de I / O são realizadas pelo código Python (apenas?) E é capaz de escolher um dos métodos de co-rotina pendentes para executar após o método de execução atual abandonar o controle (clique na
await
instrução )
Em outras palavras, aqui está minha tentativa de "transformar" alguma asyncio
sintaxe em algo mais compreensível:
async def coro(name):
print('before', name)
await asyncio.sleep()
print('after', name)
asyncio.gather(coro('first'), coro('second'))
# translated from async def coro(name)
class Coro(coroutine):
def before(self, name):
print('before', name)
def after(self, name):
print('after', name)
def __init__(self, name):
self.name = name
self.parts = self.before, self.after
self.pos = 0
def __call__():
self.parts[self.pos](self.name)
self.pos += 1
def done(self):
return self.pos == len(self.parts)
# translated from asyncio.gather()
class AsyncIOManager:
def gather(*coros):
while not every(c.done() for c in coros):
coro = random.choice(coros)
coro()
Se meu palpite estiver correto: então eu tenho um problema. Como o I / O realmente acontece neste cenário? Em um tópico separado? Todo o interpretador está suspenso e o I / O ocorre fora do interpretador? O que exatamente significa I / O? Se meu procedimento python chamou procedimento C open()
e, por sua vez, enviou interrupção ao kernel, cedendo o controle a ele, como o interpretador Python sabe sobre isso e é capaz de continuar executando algum outro código, enquanto o código do kernel faz a E / S real e até ele acorda o procedimento Python que enviou a interrupção originalmente? Como pode o interpretador Python, em princípio, estar ciente de que isso está acontecendo?
fonte
BaseEventLoop
é implementado: github.com/python/cpython/blob/…_run_once
que, que é realmente a única função útil em todo o módulo, tornou-se "privada"? A implementação é horrível, mas isso não é um problema. Por que a única função que você deseja chamar em um loop de evento está marcada como "não me chame"?_run_once
em primeiro lugar?_run_once
?asyncio
é complexo e tem seus defeitos, mas mantenha a discussão civilizada. Não fale mal dos desenvolvedores por trás do código que você mesmo não entende.Respostas:
Como funciona o assíncio?
Antes de responder a esta pergunta, precisamos entender alguns termos básicos, pule-os se você já conhece algum deles.
Geradores
Geradores são objetos que nos permitem suspender a execução de uma função python. Os geradores selecionados pelo usuário são implementados usando a palavra-chave
yield
. Ao criar uma função normal contendo ayield
palavra - chave, transformamos essa função em um gerador:Como você pode ver, chamar
next()
o gerador faz com que o interpretador carregue o frame do teste e retorne oyield
valor ed. Chamandonext()
novamente, faz com que o quadro seja carregado novamente na pilha do interpretador e continue comyield
outro valor.Na terceira vez que
next()
é chamado, nosso gerador estava pronto eStopIteration
acionado.Comunicando-se com um gerador
Uma característica menos conhecida dos geradores é o fato de que você pode se comunicar com eles usando dois métodos:
send()
ethrow()
.Ao chamar
gen.send()
, o valor é passado como um valor de retorno dayield
palavra - chave.gen.throw()
por outro lado, permite lançar Exceptions dentro de geradores, com a exceção levantada no mesmo local queyield
foi chamada.Retornando valores de geradores
Retornar um valor de um gerador, resulta no valor sendo colocado dentro da
StopIteration
exceção. Podemos posteriormente recuperar o valor da exceção e usá-lo de acordo com nossa necessidade.Eis uma nova palavra-chave:
yield from
Python 3.4 veio com a adição de uma nova palavra-chave:
yield from
. O que essa palavra-chave nos permite fazer, é passar em qualquernext()
,send()
ethrow()
em um gerador de mais interna aninhada. Se o gerador interno retornar um valor, também será o valor de retorno deyield from
:Escrevi um artigo para elaborar mais sobre este tópico.
Juntando tudo
Ao introduzir a nova palavra-chave
yield from
no Python 3.4, agora éramos capazes de criar geradores dentro de geradores que, assim como um túnel, passam os dados de um lado para outro dos geradores mais internos para os mais externos. Isso gerou um novo significado para geradores - corrotinas .Corrotinas são funções que podem ser interrompidas e retomadas durante a execução. Em Python, eles são definidos usando a
async def
palavra - chave. Assim como os geradores, eles também usam sua própria forma,yield from
que éawait
. Antesasync
e depois deawait
introduzidos no Python 3.5, criamos corrotinas exatamente da mesma maneira que os geradores foram criados (com emyield from
vez deawait
).Como todo iterador ou gerador que implementa o
__iter__()
método, as corrotinas implementam o__await__()
que permite que elas continuem sempre queawait coro
é chamado.Há um belo diagrama de sequência dentro da documentação do Python que você deve verificar.
Em asyncio, além das funções de co-rotina, temos 2 objetos importantes: tarefas e futuros .
Futuros
Futuros são objetos que têm o
__await__()
método implementado e sua função é manter um determinado estado e resultado. O estado pode ser um dos seguintes:fut.cancel()
fut.set_result()
ou por um conjunto de exceção usandofut.set_exception()
O resultado, assim como você adivinhou, pode ser um objeto Python, que será retornado, ou uma exceção que pode ser gerada.
Outra característica importante dos
future
objetos é que eles contêm um método chamadoadd_done_callback()
. Este método permite que as funções sejam chamadas assim que a tarefa for concluída - seja ela gerada uma exceção ou concluída.Tarefas
Objetos de tarefa são futuros especiais, que envolvem corrotinas e se comunicam com as corrotinas mais internas e externas. Cada vez que uma co-rotina sai do
await
futuro, o futuro é passado de volta para a tarefa (assim como emyield from
), e a tarefa o recebe.Em seguida, a tarefa se liga ao futuro. Fá-lo invocando
add_done_callback()
o futuro. A partir de agora, se o futuro estiver pronto, seja cancelado, aprovado uma exceção ou um objeto Python como resultado, o retorno de chamada da tarefa será chamado e voltará à existência.Asyncio
A última questão candente que devemos responder é - como o IO é implementado?
Bem no fundo do assíncio, temos um loop de eventos. Um ciclo de eventos de tarefas. O trabalho do loop de eventos é chamar as tarefas sempre que estiverem prontas e coordenar todo esse esforço em uma única máquina de trabalho.
A parte IO do loop de eventos é construída sobre uma única função crucial chamada
select
. Selecionar é uma função de bloqueio, implementada pelo sistema operacional subjacente, que permite aguardar nos soquetes por dados de entrada ou saída. Quando os dados são recebidos, ele desperta e retorna os soquetes que receberam os dados, ou os soquetes que estão prontos para gravação.Quando você tenta receber ou enviar dados por um soquete através do assíncio, o que realmente acontece a seguir é que o soquete é primeiro verificado se possui algum dado que pode ser lido ou enviado imediatamente. Se seu
.send()
buffer estiver cheio ou.recv()
vazio, o socket é registrado para aselect
função (simplesmente adicionando-o a uma das listas,rlist
forrecv
ewlist
forsend
) e a função apropriada éawait
umfuture
objeto recém-criado , ligado a esse socket.Quando todas as tarefas disponíveis estão esperando por futuros, o loop de eventos chama
select
e espera. Quando um dos soquetes tem dados de entrada, ou seusend
buffer é drenado, asyncio verifica o futuro objeto vinculado a esse soquete e define como concluído.Agora toda a mágica acontece. O futuro está pronto, a tarefa que se adicionou antes com
add_done_callback()
volta à vida e chama.send()
a co-rotina que retoma a co-rotina mais interna (por causa daawait
cadeia) e você lê os dados recém-recebidos de um buffer próximo. foi derramado até.Cadeia de método novamente, em caso de
recv()
:select.select
espera.future.set_result()
é chamado.add_done_callback()
agora foi ativada..send()
a co-rotina que vai até a co-rotina mais interna e a desperta.Em resumo, o asyncio usa recursos de gerador, que permitem pausar e retomar funções. Ele usa
yield from
recursos que permitem a passagem de dados para frente e para trás do gerador interno para o externo. Ele usa tudo isso para interromper a execução da função enquanto aguarda a conclusão do IO (usando aselect
função OS ).E o melhor de tudo? Enquanto uma função está em pausa, outra pode correr e intercalar com o delicado tecido, que é asyncio.
fonte
yield from
funcionamento. No entanto, observei no início que ele pode ser ignorado caso o leitor já saiba sobre ele :-) Mais alguma coisa que você acha que devo acrescentar?select
pode se qualificar, pois é assim que as chamadas de sistema de E / S sem bloqueio funcionam no sistema operacional. Asasyncio
construções reais e o loop de evento são apenas código no nível do aplicativo criado a partir dessas coisas.Falar sobre
async/await
easyncio
não é a mesma coisa. A primeira é uma construção fundamental de baixo nível (corrotinas), enquanto a última é uma biblioteca que usa essas construções. Por outro lado, não existe uma resposta definitiva única.O seguinte é uma descrição geral de como
async/await
easyncio
bibliotecas -como trabalho. Ou seja, pode haver outros truques no topo (existem ...), mas eles são irrelevantes, a menos que você os construa sozinho. A diferença deve ser insignificante, a menos que você já saiba o suficiente para não ter que fazer essa pergunta.1. Corrotinas versus sub-rotinas em uma casca de noz
Assim como as sub-rotinas (funções, procedimentos, ...), co-rotinas (geradores, ...) são uma abstração da pilha de chamadas e do ponteiro de instrução: há uma pilha de peças de código em execução e cada uma está em uma instrução específica.
A distinção de
def
versusasync def
é apenas para maior clareza. A diferença real éreturn
versusyield
. A partir disso,await
ouyield from
veja a diferença de chamadas individuais para pilhas inteiras.1.1. Sub-rotinas
Uma sub-rotina representa um novo nível de pilha para armazenar variáveis locais e uma única passagem de suas instruções para chegar ao fim. Considere uma sub-rotina como esta:
Quando você o executa, isso significa
bar
equx
return
, envia seu valor para a pilha de chamadasNotavelmente, 4. significa que uma sub-rotina sempre começa no mesmo estado. Tudo o que é exclusivo da função em si é perdido na conclusão. Uma função não pode ser retomada, mesmo se houver instruções depois
return
.1.2. Corrotinas como sub-rotinas persistentes
Uma co-rotina é como uma sub-rotina, mas pode sair sem destruir seu estado. Considere uma co-rotina como esta:
Quando você o executa, isso significa
bar
equx
yield
, envia seu valor para a pilha de chamada, mas armazena a pilha e o ponteiro de instruçãoyield
, restaure a pilha e o ponteiro de instrução e empurre os argumentos paraqux
return
, envia seu valor para a pilha de chamadasObserve a adição de 2.1 e 2.2 - uma co-rotina pode ser suspensa e reiniciada em pontos predefinidos. Isso é semelhante a como uma sub-rotina é suspensa durante a chamada de outra sub-rotina. A diferença é que a co-rotina ativa não está estritamente ligada à sua pilha de chamada. Em vez disso, uma co-rotina suspensa faz parte de uma pilha separada e isolada.
Isso significa que co-rotinas suspensas podem ser armazenadas livremente ou movidas entre pilhas. Qualquer pilha de chamadas que tenha acesso a uma co-rotina pode decidir retomá-la.
1.3. Atravessando a pilha de chamadas
Até agora, nossa co-rotina apenas desce na pilha de chamadas com
yield
. Uma sub-rotina pode descer e subir na pilha de chamadas comreturn
e()
. Para completar, as corrotinas também precisam de um mecanismo para subir na pilha de chamadas. Considere uma co-rotina como esta:Quando você o executa, isso significa que ele ainda aloca a pilha e o ponteiro de instrução como uma sub-rotina. Quando ele é suspenso, ainda é como armazenar uma sub-rotina.
No entanto,
yield from
faz ambos . Ele suspende a pilha e o ponteiro de instruçãowrap
e é executadocofoo
. Observe quewrap
permanece suspenso atécofoo
terminar completamente. Sempre quecofoo
suspende ou algo é enviado,cofoo
é conectado diretamente à pilha de chamada.1.4. Corrotinas até o fim
Conforme estabelecido,
yield from
permite conectar dois escopos em outro intermediário. Quando aplicado recursivamente, significa que o topo da pilha pode ser conectado à parte inferior da pilha.Observe isso
root
ecoro_b
não se conheçam. Isso torna as co-rotinas muito mais limpas do que os callbacks: as corrotinas ainda são construídas em uma relação 1: 1 como as sub-rotinas. As corrotinas suspendem e retomam toda a pilha de execução existente até um ponto de chamada regular.Notavelmente,
root
poderia ter um número arbitrário de corrotinas para retomar. No entanto, nunca pode retomar mais de um ao mesmo tempo. As co-rotinas da mesma raiz são concorrentes, mas não paralelas!1,5. Python's
async
eawait
A explicação até agora usou explicitamente o vocabulário
yield
eyield from
dos geradores - a funcionalidade subjacente é a mesma. A nova sintaxe Python3.5async
eawait
existe principalmente para maior clareza.As instruções
async for
easync with
são necessárias porque você quebraria ayield from/await
cadeia com as instruçõesfor
e nuaswith
.2. Anatomia de um loop de evento simples
Por si só, uma co-rotina não tem o conceito de ceder o controle a outra co-rotina. Ele só pode ceder o controle ao chamador na parte inferior de uma pilha de co-rotinas. Esse chamador pode então mudar para outra co-rotina e executá-la.
Este nó raiz de várias co-rotinas é comumente um loop de eventos : na suspensão, uma co-rotina produz um evento no qual deseja retomar. Por sua vez, o loop de eventos é capaz de esperar com eficiência que esses eventos ocorram. Isso permite que ele decida qual co-rotina executar a seguir ou como esperar antes de retomar.
Tal design implica que existe um conjunto de eventos predefinidos que o loop entende. Várias corrotinas
await
entre si, até que finalmente um evento éawait
ed. Este evento pode se comunicar diretamente com o loop de eventos por meioyield
do controle.A chave é que a suspensão da co-rotina permite que o loop de eventos e os eventos se comuniquem diretamente. A pilha de co-rotina intermediária não requer nenhum conhecimento sobre qual loop a está executando, nem como os eventos funcionam.
2.1.1. Eventos no tempo
O evento mais simples de lidar é chegar a um determinado momento. Este é um bloco fundamental de código encadeado também: um encadeamento repetidamente
sleep
até que uma condição seja verdadeira. No entanto, umasleep
execução normal bloqueia por si só - queremos que outras corrotinas não sejam bloqueadas. Em vez disso, queremos dizer ao loop de eventos quando ele deve retomar a pilha de co-rotinas atual.2.1.2. Definindo um Evento
Um evento é simplesmente um valor que podemos identificar - seja por meio de um enum, um tipo ou outra identidade. Podemos definir isso com uma classe simples que armazena nosso tempo alvo. Além de armazenar as informações do evento, podemos permitir a
await
uma classe diretamente.Essa classe apenas armazena o evento - não diz como tratá-lo de fato.
A única característica especial é
__await__
- é o que aawait
palavra - chave procura. Praticamente, é um iterador, mas não está disponível para a máquina de iteração regular.2.2.1. Esperando um evento
Agora que temos um evento, como as corrotinas reagem a ele? Devemos ser capazes de expressar o equivalente a
sleep
porawait
ing nosso evento. Para ver melhor o que está acontecendo, esperamos duas vezes na metade do tempo:Podemos instanciar e executar essa co-rotina diretamente. Semelhante a um gerador, o uso de
coroutine.send
executa a co-rotina até oyield
resultado.Isso nos dá dois
AsyncSleep
eventos e umStopIteration
quando a co-rotina é concluída. Observe que o único atraso é dotime.sleep
loop! CadaAsyncSleep
um armazena apenas um deslocamento da hora atual.2.2.2. Evento + Sono
Neste ponto, temos dois mecanismos separados à nossa disposição:
AsyncSleep
Eventos que podem ser gerados de dentro de uma co-rotinatime.sleep
que pode esperar sem impactar as corrotinasNotavelmente, esses dois são ortogonais: nenhum afeta ou ativa o outro. Como resultado, podemos criar nossa própria estratégia
sleep
para atender ao atraso de umAsyncSleep
.2.3. Um ciclo de eventos ingênuo
Se tivermos várias corrotinas, cada uma pode nos dizer quando deseja ser acordada. Podemos então esperar até que o primeiro deles queira ser retomado, depois o que vem depois e assim por diante. Notavelmente, em cada ponto, só nos importamos com qual é o próximo .
Isso torna o agendamento direto:
Uma implementação trivial não precisa de nenhum conceito avançado. A
list
permite classificar as corrotinas por data. Esperar é normaltime.sleep
. A execução de corrotinas funciona exatamente como antes comcoroutine.send
.Claro, isso tem muito espaço para melhorias. Podemos usar um heap para a fila de espera ou uma tabela de despacho para eventos. Também poderíamos buscar valores de retorno de
StopIteration
e atribuí-los à co-rotina. No entanto, o princípio fundamental permanece o mesmo.2.4. Espera Cooperativa
O
AsyncSleep
evento e orun
loop de evento são uma implementação totalmente funcional de eventos cronometrados.Isso alterna cooperativamente entre cada uma das cinco co-rotinas, suspendendo cada uma por 0,1 segundos. Mesmo que o loop de eventos seja síncrono, ele ainda executa o trabalho em 0,5 segundos em vez de 2,5 segundos. Cada co-rotina mantém o estado e atua independentemente.
3. Loop de evento de I / O
Um loop de evento compatível
sleep
é adequado para pesquisa . No entanto, esperar por E / S em um identificador de arquivo pode ser feito com mais eficiência: o sistema operacional implementa E / S e, portanto, sabe quais identificadores estão prontos. Idealmente, um loop de evento deve oferecer suporte a um evento "pronto para I / O" explícito.3.1. A
select
chamadaPython já tem uma interface para consultar o sistema operacional para ler as alças de E / S. Quando chamado com identificadores para ler ou gravar, ele retorna os identificadores prontos para ler ou gravar:
Por exemplo, podemos
open
escrever um arquivo e esperar que esteja pronto:Uma vez que o select retorna,
writeable
contém nosso arquivo aberto.3.2. Evento de I / O básico
Semelhante à
AsyncSleep
solicitação, precisamos definir um evento para E / S. Com aselect
lógica subjacente , o evento deve se referir a um objeto legível - digamos umopen
arquivo. Além disso, armazenamos quantos dados devemos ler.Como acontece com a
AsyncSleep
maioria, apenas armazenamos os dados necessários para a chamada de sistema subjacente. Desta vez,__await__
pode ser reiniciado várias vezes - até que o nosso desejadoamount
seja lido. Além disso, obtemosreturn
o resultado de E / S em vez de apenas retomar.3.3. Aumentando um loop de evento com I / O de leitura
A base para nosso loop de eventos ainda é a
run
definida anteriormente. Primeiro, precisamos rastrear as solicitações de leitura. Este não é mais um cronograma classificado, apenas mapeamos solicitações de leitura para corrotinas.Uma vez que
select.select
leva um parâmetro de tempo limite, podemos usá-lo no lugar detime.sleep
.Isso nos dá todos os arquivos legíveis - se houver algum, executamos a co-rotina correspondente. Se não houver nenhum, esperamos o suficiente para que nossa corrotina atual seja executada.
Finalmente, temos que realmente ouvir as solicitações de leitura.
3.4. Juntar as peças
O texto acima foi um pouco simplificado. Precisamos fazer algumas mudanças para não deixar as corrotinas adormecidas de fome, se sempre pudermos ler. Precisamos lidar com o fato de não termos nada para ler ou nada pelo que esperar. No entanto, o resultado final ainda se encaixa em 30 LOC.
3,5. E / S cooperativa
Os
AsyncSleep
,AsyncRead
erun
implementações estão agora totalmente funcional para dormir e / ou leitura. Da mesma forma que parasleepy
, podemos definir um auxiliar para testar a leitura:Executando isso, podemos ver que nosso I / O é intercalado com a tarefa de espera:
4. E / S sem bloqueio
Embora a E / S em arquivos transmita o conceito, não é realmente adequado para uma biblioteca como
asyncio
: aselect
chamada sempre retorna para arquivos , e ambosopen
eread
pode bloquear indefinidamente . Isso bloqueia todas as co-rotinas de um loop de evento - o que é ruim. Bibliotecas como oaiofiles
uso de threads e sincronização para falsificar E / S sem bloqueio e eventos no arquivo.No entanto, os soquetes permitem E / S sem bloqueio - e sua latência inerente o torna muito mais crítico. Quando usado em um loop de evento, a espera por dados e a nova tentativa podem ser quebradas sem bloquear nada.
4.1. Evento de I / O sem bloqueio
Semelhante ao nosso
AsyncRead
, podemos definir um evento suspend-and-read para sockets. Em vez de pegar um arquivo, pegamos um socket - que deve ser não bloqueador. Além disso, nossos__await__
usos emsocket.recv
vez defile.read
.Em contraste com
AsyncRead
,__await__
executa E / S verdadeiramente sem bloqueio. Quando os dados estão disponíveis, ele sempre lê. Quando nenhum dado está disponível, ele sempre é suspenso. Isso significa que o loop de eventos só é bloqueado enquanto realizamos um trabalho útil.4.2. Desbloqueando o loop de eventos
No que diz respeito ao loop de eventos, nada muda muito. O evento a ser escutado ainda é o mesmo que o dos arquivos - um descritor de arquivo marcado como pronto por
select
.Neste ponto, deve ser óbvio que
AsyncRead
eAsyncRecv
são o mesmo tipo de evento. Poderíamos facilmente refatorá-los para ser um evento com um componente de E / S intercambiável. Na verdade, o loop de eventos, as corrotinas e os eventos separam claramente um planejador, código intermediário arbitrário e a E / S real.4.3. O lado feio do I / O sem bloqueio
Em princípio, o que você deve fazer neste ponto é replicar a lógica de
read
como umrecv
paraAsyncRecv
. No entanto, isso é muito mais feio agora - você tem que lidar com os retornos iniciais quando as funções bloqueiam dentro do kernel, mas fornecem o controle para você. Por exemplo, abrir uma conexão em vez de abrir um arquivo é muito mais demorado:Para encurtar a história, o que resta são algumas dezenas de linhas de tratamento de exceções. Os eventos e o loop de eventos já funcionam neste ponto.
Termo aditivo
Exemplo de código no github
fonte
yield self
em AsyncSleep me dáTask got back yield
erro, por que isso? Vejo que o código em asyncio.Futures usa isso. Usar um rendimento mínimo funciona bem.Seu
coro
desugaring é conceitualmente correto, mas ligeiramente incompleto.await
não suspende incondicionalmente, mas apenas se encontrar uma chamada de bloqueio. Como ele sabe que uma chamada está bloqueando? Isso é decidido pelo código que está sendo aguardado. Por exemplo, uma implementação aguardada de leitura de soquete pode ser desugared para:No assíncio real, o código equivalente modifica o estado de a em
Future
vez de retornar valores mágicos, mas o conceito é o mesmo. Quando apropriadamente adaptado a um objeto do tipo gerador, o código acima pode serawait
editado.Do lado do chamador, quando sua co-rotina contém:
Ele se transforma em algo próximo a:
Pessoas familiarizadas com geradores tendem a descrever o que precede em termos de
yield from
qual faz a suspensão automaticamente.A cadeia de suspensão continua todo o caminho até o loop de evento, que percebe que a co-rotina está suspensa, remove-a do conjunto executável e continua a executar corrotinas executáveis, se houver. Se nenhuma co-rotina puder ser executada, o loop espera
select()
até que um descritor de arquivo no qual uma co-rotina esteja interessada esteja pronto para E / S. (O loop de eventos mantém um mapeamento de descritor de arquivo para corrotina.)No exemplo acima, uma vez que
select()
diz ao loop de eventos quesock
é legível, ele será adicionado novamentecoro
ao conjunto executável, de forma que continuará a partir do ponto de suspensão.Em outras palavras:
Tudo acontece no mesmo segmento por padrão.
O loop de eventos é responsável por programar as corrotinas e despertá-las quando o que quer que estejam esperando (normalmente uma chamada IO que normalmente seria bloqueada ou um tempo limite) estiver pronto.
Para insights sobre os loops de evento de direção de corrotina, recomendo esta palestra de Dave Beazley, onde ele demonstra a codificação de um loop de evento do zero na frente do público ao vivo.
fonte
async.wait_for()
que não faz o que deveria ... Por que é um problema tão grande adicionar um retorno de chamada ao loop de evento e informá-lo processar quantos retornos de chamada forem necessários, incluindo o que você acabou de adicionar? Minha frustração comasyncio
é em parte devido ao fato de que o conceito subjacente é muito simples e, por exemplo, o Emacs Lisp teve implementação por muito tempo, sem usar chavões ... (ou sejacreate-async-process
eaccept-process-output
- e isso é tudo o que é necessário ... (cont.)wait_for
não faça o que deveria (faz, é uma corrotina que você deve esperar), é que suas expectativas não correspondem ao que o sistema foi projetado e implementado para fazer. Acho que seu problema poderia ser comparado a asyncio se o loop de eventos estivesse em execução em um thread separado, mas não sei os detalhes do seu caso de uso e, honestamente, sua atitude não torna muito divertido ajudá-lo.My frustration with asyncio is in part due to the fact that the underlying concept is very simple, and, for example, Emacs Lisp had implementation for ages, without using buzzwords...
- Nada impede você de implementar este conceito simples sem chavões para o Python, então :) Por que você usa esse asyncio feio afinal? Implemente o seu do zero. Por exemplo, você pode começar criando sua própriaasync.wait_for()
função que faz exatamente o que deveria.asyncio
. Mas, em princípio, essa decisão não é minha. Sou coagido a usar linguagem lixo por meio de en.wikipedia.org/wiki/Ultimatum_game .Tudo se resume aos dois principais desafios que asyncio está enfrentando:
A resposta ao primeiro ponto já existe há muito tempo e é chamada de loop de seleção . Em python, é implementado no módulo seletores .
A segunda questão está relacionada ao conceito de co-rotina , ou seja, funções que podem interromper sua execução e ser restauradas posteriormente. Em python, as corrotinas são implementadas usando geradores e o rendimento da instrução. Isso é o que se esconde por trás da sintaxe async / await .
Mais recursos nesta resposta .
EDIT: Endereçando seu comentário sobre goroutines:
O equivalente mais próximo de uma goroutina em asyncio não é, na verdade, uma co-rotina, mas uma tarefa (veja a diferença na documentação ). Em python, uma co-rotina (ou gerador) não sabe nada sobre os conceitos de loop de evento ou I / O. É simplesmente uma função que pode interromper sua execução
yield
enquanto mantém seu estado atual, para que possa ser restaurada posteriormente. Ayield from
sintaxe permite encadea-los de forma transparente.Agora, dentro de uma tarefa de assíncio, a co-rotina na base da cadeia sempre acaba rendendo um futuro . Esse futuro então borbulha no ciclo de eventos e é integrado ao mecanismo interno. Quando o futuro é definido como concluído por algum outro retorno de chamada interno, o loop de eventos pode restaurar a tarefa enviando o futuro de volta à cadeia de co-rotina.
EDIT: Resolvendo algumas das perguntas em sua postagem:
Não, nada acontece em um tópico. A E / S é sempre gerenciada pelo loop de eventos, principalmente por meio de descritores de arquivo. No entanto, o registro desses descritores de arquivo é geralmente oculto por corrotinas de alto nível, fazendo o trabalho sujo para você.
Uma E / S é qualquer chamada de bloqueio. No assíncio, todas as operações de I / O devem passar pelo loop de eventos, pois como você disse, o loop de eventos não tem como saber que uma chamada de bloqueio está sendo realizada em algum código síncrono. Isso significa que você não deve usar um síncrono
open
no contexto de uma co-rotina. Em vez disso, use uma biblioteca dedicada, como aiofiles, que fornece uma versão assíncrona deopen
.fonte
yield from
não diz nada.yield from
é apenas uma construção de sintaxe, não é um bloco de construção fundamental que os computadores podem executar. Da mesma forma, para selecionar o loop. Sim, as corrotinas em Go também usam loop de seleção, mas o que eu estava tentando fazer funcionaria em Go, mas não em Python. Preciso de respostas mais detalhadas para entender por que não funcionou.asyncio
, para mim, se resumiria a um código C que ilustra o que a sintaxe do Python foi traduzida.