Esta pregunta está motivada por mi otra pregunta: ¿Cómo esperar en cdef?
Hay toneladas de artículos y publicaciones de blogs en la web asyncio
, pero todos son muy superficiales. No pude encontrar ninguna información sobre cómo asyncio
se implementa realmente y qué hace que la E / S sea asincrónica. Estaba tratando de leer el código fuente, pero son miles de líneas que no son del código C de mayor grado, muchas de las cuales tratan con objetos auxiliares, pero lo más importante es que es difícil conectarse entre la sintaxis de Python y el código C que traduciría. dentro.
La propia documentación de Asycnio es aún menos útil. No hay información sobre cómo funciona, solo algunas pautas sobre cómo usarlo, que a veces también son engañosas / están muy mal escritas.
Estoy familiarizado con la implementación de corrutinas de Go, y esperaba que Python hiciera lo mismo. Si ese fuera el caso, el código que aparecí en la publicación vinculada anteriormente habría funcionado. Como no fue así, ahora estoy tratando de averiguar por qué. Mi mejor suposición hasta ahora es la siguiente, corríjame donde me equivoque:
- Las definiciones de procedimiento del formulario en
async def foo(): ...
realidad se interpretan como métodos de una clase heredadacoroutine
. - Quizás, en
async def
realidad , se divide en varios métodos medianteawait
declaraciones, donde el objeto, en el que se llaman estos métodos, puede realizar un seguimiento del progreso realizado hasta el momento en la ejecución. - Si lo anterior es cierto, entonces, esencialmente, la ejecución de una corrutina se reduce a llamar a los métodos de un objeto de rutina por algún administrador global (¿bucle?).
- El administrador global es de alguna manera (¿cómo?) Consciente de cuándo las operaciones de E / S son realizadas por el código Python (¿solo?) Y puede elegir uno de los métodos de corutina pendientes para ejecutar después de que el método de ejecución actual haya renunciado al control (presione en la
await
declaración ).
En otras palabras, aquí está mi intento de "desugaring" de alguna asyncio
sintaxis en algo más comprensible:
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()
Si mi conjetura resulta correcta: entonces tengo un problema. ¿Cómo ocurre realmente la E / S en este escenario? ¿En un hilo separado? ¿Está suspendido todo el intérprete y la E / S ocurre fuera del intérprete? ¿Qué se entiende exactamente por E / S? Si mi procedimiento de Python llamó al procedimiento C open()
, y a su vez envió una interrupción al kernel, cediéndole el control, ¿cómo sabe el intérprete de Python sobre esto y puede continuar ejecutando algún otro código, mientras que el código del kernel hace la E / S real y hasta ¿Despierta el procedimiento de Python que envió la interrupción originalmente? ¿Cómo puede el intérprete de Python, en principio, darse cuenta de que esto está sucediendo?
fuente
BaseEventLoop
se implementa CPython : github.com/python/cpython/blob/…_run_once
hace "privada", que es en realidad la única función útil en todo este módulo? La implementación es horrible, pero eso es un problema menor. ¿Por qué la única función a la que querría llamar en un bucle de eventos está marcada como "no me llames"?_run_once
en primer lugar?_run_once
?asyncio
es complejo y tiene sus fallas, pero por favor mantenga la discusión civilizada. No hable mal de los desarrolladores detrás del código que usted mismo no comprende.Respuestas:
¿Cómo actúa asyncio?
Antes de responder a esta pregunta, debemos comprender algunos términos básicos, omítelos si ya conoce alguno de ellos.
Generadores
Los generadores son objetos que nos permiten suspender la ejecución de una función de Python. Los generadores seleccionados por el usuario se implementan utilizando la palabra clave
yield
. Al crear una función normal que contiene layield
palabra clave, convertimos esa función en un generador:Como puede ver, llamar
next()
al generador hace que el intérprete cargue el marco de prueba y devuelva elyield
valor ed. Llamar denext()
nuevo, hace que el marco se cargue de nuevo en la pila de intérpretes y continúeyield
con otro valor.A la tercera vez que
next()
se llama, nuestro generador se terminó yStopIteration
se lanzó.Comunicarse con un generador
Una característica menos conocida de los generadores es el hecho de que puede comunicarse con ellos mediante dos métodos:
send()
ythrow()
.Al llamar
gen.send()
, el valor se pasa como un valor de retorno delyield
palabra clave.gen.throw()
por otro lado, permite lanzar Excepciones dentro de los generadores, con la excepción levantada en el mismo lugaryield
se llamó.Devolución de valores de generadores
Al devolver un valor de un generador, el valor se coloca dentro de la
StopIteration
excepción. Más adelante podemos recuperar el valor de la excepción y usarlo según nuestras necesidades.He aquí una nueva palabra clave:
yield from
Python 3.4 vino con la adición de una nueva palabra clave:
yield from
. ¿Qué palabra clave que nos permite hacer, es pasar en cualquiernext()
,send()
ythrow()
en un generador de más interno anidado. Si el generador interno devuelve un valor, también es el valor de retorno deyield from
:He escrito un articulo para profundizar en este tema.
Poniendolo todo junto
Al introducir la nueva palabra clave
yield from
en Python 3.4, ahora pudimos crear generadores dentro de generadores que, al igual que un túnel, pasan los datos de un lado a otro de los generadores más internos a los más externos. Esto ha dado lugar a un nuevo significado para los generadores: corrutinas .Las corrutinas son funciones que se pueden detener y reanudar mientras se ejecutan. En Python, se definen mediante la
async def
palabra clave. Al igual que los generadores, ellos también usan su propia forma deyield from
lo que esawait
. Antesasync
yawait
se introdujeron en Python 3.5, creamos corrutinas exactamente de la misma manera que se crearon los generadores (con enyield from
lugar deawait
).Como todos los iteradores o generadores que implementan el
__iter__()
método, se implementan corrutinas__await__()
que les permiten continuar cada vezawait coro
se llama.Hay un bonito diagrama de secuencia dentro del documentos de Python que debe consultar.
En asyncio, además de las funciones de rutina, tenemos 2 objetos importantes: tareas y futuros .
Futuros
Los futuros son objetos que tienen el
__await__()
método implementado y su trabajo es mantener un cierto estado y resultado. El estado puede ser uno de los siguientes:fut.cancel()
fut.set_result()
o por un conjunto de excepciones usandofut.set_exception()
El resultado, tal como lo ha adivinado, puede ser un objeto Python, que se devolverá, o una excepción que se puede generar.
Otra característica importante de los
future
objetos es que contienen un método llamadoadd_done_callback()
. Este método permite llamar a las funciones tan pronto como se realiza la tarea, ya sea que haya generado una excepción o haya terminado.Tareas
Los objetos de tarea son futuros especiales, que envuelven corrutinas y se comunican con las corrutinas más internas y más externas. Cada vez que una corrutina
await
es un futuro, el futuro se devuelve a la tarea (como enyield from
) y la tarea lo recibe.A continuación, la tarea se une al futuro. Lo hace llamando
add_done_callback()
el futuro. A partir de ahora, si el futuro se realiza alguna vez, ya sea cancelando, pasando una excepción o pasando un objeto Python como resultado, se llamará a la devolución de llamada de la tarea y volverá a existir.Asyncio
La última pregunta candente que debemos responder es: ¿cómo se implementa el IO?
En el fondo de asyncio, tenemos un bucle de eventos. Un ciclo de eventos de tareas. El trabajo del bucle de eventos es llamar a las tareas cada vez que están listas y coordinar todo ese esfuerzo en una sola máquina de trabajo.
La parte IO del bucle de eventos se basa en una única función crucial llamada
select
. Select es una función de bloqueo, implementada por el sistema operativo debajo, que permite esperar en los sockets para los datos entrantes o salientes. Una vez que se reciben los datos, se activa y devuelve los sockets que recibieron datos o los sockets que están listos para escribir.Cuando intenta recibir o enviar datos a través de un socket a través de asyncio, lo que realmente sucede a continuación es que el socket se verifica primero si tiene algún dato que pueda leerse o enviarse inmediatamente. Si su
.send()
búfer está lleno, o el.recv()
búfer está vacío, el conector se registra en laselect
función (simplemente agregándolo a una de las listas,rlist
pararecv
ywlist
parasend
) y la función apropiada esawait
unfuture
objeto recién creado , vinculado a ese conector.Cuando todas las tareas disponibles están esperando futuros, el ciclo de eventos llama
select
y espera. Cuando uno de los sockets tiene datos entrantes, o susend
búfer se agota, asyncio busca el objeto futuro vinculado a ese socket y lo configura como hecho.Ahora ocurre toda la magia. El futuro está listo para terminar, la tarea que se agregó antes con
add_done_callback()
vuelve a la vida y llama.send()
a la corrutina que reanuda la corrutina más interna (debido a laawait
cadena) y usted lee los datos recién recibidos de un búfer cercano. se derramó sobre.Cadena de métodos nuevamente, en caso de
recv()
:select.select
murga.future.set_result()
se llama.add_done_callback()
ahora está despertada..send()
a la corrutina que llega hasta la corrutina más interna y la activa.En resumen, asyncio usa capacidades de generador, que permiten pausar y reanudar funciones. Utiliza
yield from
capacidades que permiten pasar datos de ida y vuelta desde el generador más interno al más externo. Utiliza todos esos para detener la ejecución de la función mientras espera que IO se complete (mediante el uso de laselect
función del sistema operativo).¿Y lo mejor de todo? Mientras una función está en pausa, otra puede ejecutarse y entrelazarse con la delicada tela, que es asyncio.
fuente
yield from
funcionamiento del generador . Sin embargo, noté arriba que se puede omitir en caso de que el lector ya lo sepa :-) ¿Algo más que crea que debería agregar?select
puede calificar, ya que así es como funcionan las llamadas al sistema de E / S sin bloqueo en el sistema operativo. Lasasyncio
construcciones reales y el bucle de eventos son solo código de nivel de aplicación creado a partir de estas cosas.Hablar de
async/await
yasyncio
no es lo mismo. La primera es una construcción fundamental de bajo nivel (corrutinas), mientras que la última es una biblioteca que utiliza estas construcciones. Por el contrario, no existe una única respuesta definitiva.La siguiente es una descripción general de cómo funcionan las bibliotecas
async/await
yasyncio
similares. Es decir, puede haber otros trucos en la parte superior (hay ...) pero son intrascendentes a menos que los construya usted mismo. La diferencia debería ser insignificante a menos que ya sepa lo suficiente como para no tener que hacer esa pregunta.1. Corutinas versus subrutinas en pocas palabras
Al igual que las subrutinas (funciones, procedimientos, ...), las corrutinas (generadores, ...) son una abstracción de la pila de llamadas y el puntero de instrucción: hay una pila de piezas de código en ejecución, y cada una está en una instrucción específica.
La distinción de
def
versusasync def
es simplemente para mayor claridad. La diferencia real esreturn
versusyield
. A partir de esto,await
oyield from
tome la diferencia de llamadas individuales a pilas completas.1.1. Subrutinas
Una subrutina representa un nuevo nivel de pila para contener variables locales y un solo recorrido de sus instrucciones para llegar a un final. Considere una subrutina como esta:
Cuando lo ejecutas, eso significa
bar
yqux
return
, empuja su valor a la pila de llamadasEn particular, 4. significa que una subrutina siempre comienza en el mismo estado. Todo lo exclusivo de la función en sí se pierde al finalizar. No se puede reanudar una función, incluso si hay instrucciones después
return
.1.2. Corutinas como subrutinas persistentes
Una corrutina es como una subrutina, pero puede salir sin destruir su estado. Considere una corrutina como esta:
Cuando lo ejecutas, eso significa
bar
yqux
yield
, empuja su valor a la pila de llamadas pero almacena la pila y el puntero de instrucciónyield
, restaure la pila y el puntero de instrucción y envíe argumentos aqux
return
, empuja su valor a la pila de llamadasTenga en cuenta la adición de 2.1 y 2.2: una corrutina se puede suspender y reanudar en puntos predefinidos. Esto es similar a cómo se suspende una subrutina durante la llamada a otra subrutina. La diferencia es que la corrutina activa no está estrictamente ligada a su pila de llamadas. En cambio, una corrutina suspendida es parte de una pila separada y aislada.
Esto significa que las corrutinas suspendidas se pueden almacenar o mover libremente entre pilas. Cualquier pila de llamadas que tenga acceso a una corrutina puede decidir reanudarla.
1.3. Atravesando la pila de llamadas
Hasta ahora, nuestra corrutina solo baja en la pila de llamadas con
yield
. Una subrutina puede subir y bajar en la pila de llamadas conreturn
y()
. Para que estén completas, las corrutinas también necesitan un mecanismo para subir la pila de llamadas. Considere una corrutina como esta:Cuando lo ejecuta, eso significa que todavía asigna la pila y el puntero de instrucción como una subrutina. Cuando se suspende, sigue siendo como almacenar una subrutina.
Sin embargo,
yield from
hace ambas cosas . Suspende la pila y el puntero de instrucciónwrap
y se ejecutacofoo
. Tenga en cuenta quewrap
permanece suspendido hasta quecofoo
termina por completo. Siempre que secofoo
suspende o se envía algo,cofoo
se conecta directamente a la pila de llamadas.1.4. Coroutines hasta el final
Según lo establecido,
yield from
permite conectar dos visores a través de otro intermedio. Cuando se aplica de forma recursiva, eso significa que la parte superior de la pila se puede conectar a la parte inferior de la pila.Tenga en cuenta que
root
ycoro_b
no se conocen el uno al otro. Esto hace que las corrutinas sean mucho más limpias que las devoluciones de llamada: las corrutinas aún se construyen en una relación 1: 1 como las subrutinas. Las corrutinas suspenden y reanudan toda su pila de ejecución existente hasta un punto de llamada regular.En particular,
root
podría tener un número arbitrario de corrutinas para reanudar. Sin embargo, nunca puede reanudar más de uno al mismo tiempo. Las corrutinas de la misma raíz son concurrentes pero no paralelas.1.5. Python
async
yawait
Hasta ahora, la explicación ha utilizado explícitamente el vocabulario
yield
yyield from
de los generadores: la funcionalidad subyacente es la misma. La nueva sintaxis de Python3.5async
yawait
existe principalmente para mayor claridad.Las declaraciones
async for
yasync with
son necesarias porque rompería layield from/await
cadena con las declaraciones desnudasfor
ywith
.2. Anatomía de un bucle de eventos simple
Por sí misma, una corrutina no tiene el concepto de ceder el control a otra corrutina. Solo puede ceder el control a la persona que llama en la parte inferior de una pila de corrutinas. Esta persona que llama puede cambiar a otra corrutina y ejecutarla.
Este nodo raíz de varias corrutinas es comúnmente un bucle de eventos : en suspensión, una corrutina produce un evento en el que desea reanudar. A su vez, el bucle de eventos es capaz de esperar eficientemente a que ocurran estos eventos. Esto le permite decidir qué corrutina ejecutar a continuación o cómo esperar antes de reanudar.
Tal diseño implica que existe un conjunto de eventos predefinidos que el bucle comprende. Varias corrutinas
await
entre sí, hasta que finalmente se edita un eventoawait
. Este evento puede comunicarse directamente con el bucle de eventos mediante elyield
control.La clave es que la suspensión de rutina permite que el bucle de eventos y los eventos se comuniquen directamente. La pila de corrutinas intermedia no requiere ningún conocimiento sobre qué bucle lo está ejecutando, ni cómo funcionan los eventos.
2.1.1. Eventos en el tiempo
El evento más simple de manejar es llegar a un punto en el tiempo. Este es un bloque fundamental de código enhebrado también: un subproceso se repite repetidamente
sleep
hasta que una condición es verdadera. Sin embargo, unasleep
ejecución de bloques regular por sí sola: queremos que no se bloqueen otras corrutinas. En su lugar, queremos decirle al bucle de eventos cuándo debe reanudar la pila de corrutinas actual.2.1.2. Definición de un evento
Un evento es simplemente un valor que podemos identificar, ya sea a través de una enumeración, un tipo u otra identidad. Podemos definir esto con una clase simple que almacena nuestro tiempo objetivo. Además de almacenar la información del evento, podemos permitir
await
una clase directamente.Esta clase solo almacena el evento, no dice cómo manejarlo realmente.
La única característica especial es
__await__
: es lo queawait
busca la palabra clave. Prácticamente, es un iterador pero no está disponible para la maquinaria de iteración regular.2.2.1. Esperando un evento
Ahora que tenemos un evento, ¿cómo reaccionan las corrutinas? Debemos ser capaces de expresar el equivalente de
sleep
porawait
ing nuestro evento. Para ver mejor lo que está pasando, esperamos dos veces la mitad del tiempo:Podemos instanciar y ejecutar directamente esta corrutina. Similar a un generador, el uso
coroutine.send
ejecuta la corrutina hasta obteneryield
un resultado.Esto nos da dos
AsyncSleep
eventos y luego unaStopIteration
cuando se realiza la corrutina. ¡Tenga en cuenta que el único retraso estime.sleep
el del bucle! Cada unoAsyncSleep
solo almacena un desplazamiento de la hora actual.2.2.2. Evento + Sueño
En este punto, tenemos dos mecanismos separados a nuestra disposición:
AsyncSleep
Eventos que se pueden generar desde el interior de una corrutinatime.sleep
que puede esperar sin afectar las rutinasEn particular, estos dos son ortogonales: ninguno afecta ni desencadena al otro. Como resultado, podemos idear nuestra propia estrategia
sleep
para afrontar el retraso de unAsyncSleep
.2.3. Un bucle de eventos ingenuo
Si disponemos de varias corrutinas, cada una puede indicarnos cuándo quiere que le despierten. Luego podemos esperar hasta que el primero de ellos quiera reanudarse, luego el siguiente, y así sucesivamente. En particular, en cada punto solo nos preocupamos por cuál es el siguiente .
Esto hace que la programación sea sencilla:
Una implementación trivial no necesita conceptos avanzados. A
list
permite ordenar las corrutinas por fecha. Esperar es algo habitualtime.sleep
. La ejecución de corrutinas funciona igual que antescoroutine.send
.Por supuesto, esto tiene un amplio margen de mejora. Podemos usar un montón para la cola de espera o una tabla de despacho para eventos. También podríamos obtener valores de retorno de
StopIteration
y asignarlos a la corrutina. Sin embargo, el principio fundamental sigue siendo el mismo.2.4. Espera cooperativa
El
AsyncSleep
evento y elrun
ciclo de eventos son una implementación totalmente funcional de eventos cronometrados.Esto cambia cooperativamente entre cada una de las cinco corrutinas, suspendiendo cada una durante 0,1 segundos. Aunque el ciclo de eventos es síncrono, aún ejecuta el trabajo en 0,5 segundos en lugar de 2,5 segundos. Cada corrutina mantiene el estado y actúa de forma independiente.
3. Bucle de eventos de E / S
Un bucle de eventos que admita
sleep
es adecuado para el sondeo . Sin embargo, la espera de E / S en un identificador de archivo se puede hacer de manera más eficiente: el sistema operativo implementa E / S y, por lo tanto, sabe qué identificadores están listos. Idealmente, un bucle de eventos debería admitir un evento explícito "listo para E / S".3.1. La
select
llamadaPython ya tiene una interfaz para consultar el sistema operativo para leer identificadores de E / S. Cuando se llama con identificadores para leer o escribir, devuelve los identificadores listos para leer o escribir:
Por ejemplo, podemos
open
escribir un archivo y esperar a que esté listo:Una vez que seleccione las devoluciones,
writeable
contiene nuestro archivo abierto.3.2. Evento de E / S básico
Similar a la
AsyncSleep
solicitud, necesitamos definir un evento para E / S. Con laselect
lógica subyacente , el evento debe referirse a un objeto legible, digamos unopen
archivo. Además, almacenamos cuántos datos leer.Al igual que con la
AsyncSleep
mayoría de las veces, solo almacenamos los datos necesarios para la llamada al sistema subyacente. Esta vez,__await__
se puede reanudar varias veces, hasta queamount
se haya leído lo deseado . Además, obtenemosreturn
el resultado de E / S en lugar de simplemente reanudarlo.3.3. Aumento de un bucle de eventos con lectura de E / S
La base de nuestro bucle de eventos sigue siendo la
run
definida anteriormente. Primero, necesitamos rastrear las solicitudes de lectura. Este ya no es un horario ordenado, solo asignamos solicitudes de lectura a corrutinas.Dado que
select.select
toma un parámetro de tiempo de espera, podemos usarlo en lugar detime.sleep
.Esto nos da todos los archivos legibles; si hay alguno, ejecutamos la corrutina correspondiente. Si no hay ninguno, hemos esperado lo suficiente para que se ejecute nuestra corrutina actual.
Finalmente, tenemos que escuchar las solicitudes de lectura.
3.4. Poniendo todo junto
Lo anterior fue un poco simplificado. Necesitamos hacer algunos cambios para no morir de hambre a las corrutinas para dormir si siempre podemos leer. Necesitamos manejar no tener nada que leer o nada que esperar. Sin embargo, el resultado final todavía encaja en 30 LOC.
3.5. E / S cooperativa
Las implementaciones
AsyncSleep
,AsyncRead
yrun
ahora son completamente funcionales para dormir y / o leer. Igual que parasleepy
, podemos definir un ayudante para probar la lectura:Al ejecutar esto, podemos ver que nuestra E / S está intercalada con la tarea en espera:
4. E / S sin bloqueo
Si bien la E / S en archivos transmite el concepto, no es realmente adecuado para una biblioteca como
asyncio
: laselect
llamada siempre regresa para los archivos , y ambosopen
yread
pueden bloquearse indefinidamente . Esto bloquea todas las corrutinas de un bucle de eventos, lo cual es malo. Bibliotecas comoaiofiles
utilizan subprocesos y sincronización para falsificar eventos y E / S no bloqueantes en el archivo.Sin embargo, los sockets permiten E / S sin bloqueo, y su latencia inherente lo hace mucho más crítico. Cuando se usa en un bucle de eventos, la espera de datos y el reintento se pueden ajustar sin bloquear nada.
4.1. Evento de E / S sin bloqueo
Similar a nuestro
AsyncRead
, podemos definir un evento de suspensión y lectura para sockets. En lugar de tomar un archivo, tomamos un socket, que debe ser sin bloqueo. Además, nuestros__await__
usos ensocket.recv
lugar defile.read
.A diferencia de
AsyncRead
,__await__
realiza E / S verdaderamente sin bloqueo. Cuando hay datos disponibles, siempre se lee. Cuando no hay datos disponibles, siempre se suspende. Eso significa que el bucle de eventos solo se bloquea mientras realizamos un trabajo útil.4.2. Desbloquear el bucle de eventos
En lo que respecta al bucle de eventos, nada cambia mucho. El evento para escuchar sigue siendo el mismo que para los archivos: un descriptor de archivo marcado como listo por
select
.En este punto, debería ser obvio que
AsyncRead
yAsyncRecv
son el mismo tipo de evento. Podríamos refactorizarlos fácilmente para que sean un evento con un componente de E / S intercambiable. En efecto, el ciclo de eventos, las corrutinas y los eventos separan claramente un programador, un código intermedio arbitrario y la E / S real.4.3. El lado feo de la E / S sin bloqueo
En principio, lo que deberías hacer en este punto es replicar la lógica de
read
as arecv
forAsyncRecv
. Sin embargo, esto es mucho más feo ahora: tienes que manejar los retornos tempranos cuando las funciones se bloquean dentro del kernel, pero te dan el control. Por ejemplo, abrir una conexión en lugar de abrir un archivo es mucho más largo:En pocas palabras, lo que queda son unas pocas docenas de líneas de manejo de excepciones. Los eventos y el ciclo de eventos ya funcionan en este punto.
Apéndice
Código de ejemplo en github
fuente
yield self
en AsyncSleep me da unTask got back yield
error, ¿por qué? Veo que el código en asyncio.Futures usa eso. Usar una producción pura funciona bien.Su
coro
desugaring es conceptualmente correcto, pero un poco incompleto.await
no se suspende incondicionalmente, pero solo si encuentra una llamada de bloqueo. ¿Cómo sabe que se está bloqueando una llamada? Esto se decide por el código que se espera. Por ejemplo, se podría desaconsejar una implementación esperada de lectura de socket para:En asyncio real, el código equivalente modifica el estado de a en
Future
lugar de devolver valores mágicos, pero el concepto es el mismo. Cuando se adapta adecuadamente a un objeto similar a un generador, el código anterior se puedeawait
editar.En el lado de la persona que llama, cuando su corrutina contiene:
Se convierte en algo parecido a:
Las personas familiarizadas con los generadores tienden a describir lo anterior en términos de
yield from
cuál hace la suspensión automáticamente.La cadena de suspensión continúa hasta el bucle de eventos, que advierte que la corrutina está suspendida, la elimina del conjunto ejecutable y continúa para ejecutar las corrutinas que se pueden ejecutar, si las hay. Si no se pueden ejecutar corrutinas, el bucle espera
select()
hasta que un descriptor de archivo en el que una corrutina está interesada esté listo para IO. (El bucle de eventos mantiene una asignación de descriptor de archivo a una rutina).En el ejemplo anterior, una vez que
select()
le dice al bucle de eventos quesock
es legible, se volverá a agregarcoro
al conjunto ejecutable, por lo que continuará desde el punto de suspensión.En otras palabras:
Todo sucede en el mismo hilo por defecto.
El bucle de eventos es responsable de programar las corrutinas y despertarlas cuando lo que sea que estaban esperando (generalmente una llamada IO que normalmente se bloquearía o un tiempo de espera) esté listo.
Para obtener información sobre los bucles de eventos de conducción de corrutinas, recomiendo esta charla de Dave Beazley, donde demuestra la codificación de un bucle de eventos desde cero frente a una audiencia en vivo.
fuente
async.wait_for()
no hace lo que se supone que debe hacer ... ¿Por qué es un problema tan grande agregar una devolución de llamada al bucle de eventos y decirlo? para procesar todas las devoluciones de llamada que necesite, incluida la que acaba de agregar? Mi frustración conasyncio
se debe en parte al hecho de que el concepto subyacente es muy simple y, por ejemplo, Emacs Lisp se implementó durante años, sin usar palabras de moda ... (es decir,create-async-process
yaccept-process-output
- y esto es todo lo que se necesita ... (cont.)wait_for
no haga lo que se supone que debe hacer (lo hace, es una corrutina que se supone que debe esperar), es que sus expectativas no coinciden con lo que el sistema fue diseñado e implementado para hacer. Creo que su problema podría coincidir con asyncio si el bucle de eventos se ejecutara en un hilo separado, pero no conozco los detalles de su caso de uso y, honestamente, su actitud no hace que sea muy divertido ayudarlo.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...
- Entonces , nada te impide implementar este concepto simple sin palabras de moda para Python :) ¿Por qué usas este asincio feo? Implementa el tuyo desde cero. Por ejemplo, puede comenzar creando su propiaasync.wait_for()
función que haga exactamente lo que se supone que debe hacer.asyncio
. Pero, en principio, esa no es mi decisión. Me obligan a usar lenguaje basura a través de en.wikipedia.org/wiki/Ultimatum_game .Todo se reduce a los dos desafíos principales que asyncio está abordando:
La respuesta al primer punto ha existido durante mucho tiempo y se llama ciclo de selección . En Python, se implementa en el módulo de selectores .
La segunda pregunta está relacionada con el concepto de corrutina , es decir, funciones que pueden detener su ejecución y ser restauradas posteriormente. En Python, las corrutinas se implementan usando generadores y el rendimiento de la declaración. Eso es lo que se esconde detrás de la sintaxis async / await .
Más recursos en esta respuesta .
EDITAR: Abordar su comentario sobre goroutines:
El equivalente más cercano a una goroutine en asyncio no es en realidad una corrutina sino una tarea (vea la diferencia en la documentación ). En Python, una corrutina (o un generador) no sabe nada sobre los conceptos de bucle de eventos o E / S. Simplemente es una función que puede detener su ejecución
yield
mientras mantiene su estado actual, por lo que se puede restaurar más adelante. Layield from
sintaxis permite encadenarlos de forma transparente.Ahora, dentro de una tarea de asyncio, la corrutina en la parte inferior de la cadena siempre termina dando un futuro . Este futuro luego sube al bucle de eventos y se integra en la maquinaria interna. Cuando el futuro está configurado como hecho por otra devolución de llamada interna, el bucle de eventos puede restaurar la tarea enviando el futuro de regreso a la cadena de corrutinas.
EDITAR: Abordar algunas de las preguntas en su publicación:
No, no pasa nada en un hilo. La E / S siempre es administrada por el bucle de eventos, principalmente a través de descriptores de archivo. Sin embargo, el registro de esos descriptores de archivo suele estar oculto por corrutinas de alto nivel, lo que le hace el trabajo sucio.
Una E / S es cualquier llamada de bloqueo. En asyncio, todas las operaciones de E / S deben pasar por el bucle de eventos, porque como dijiste, el bucle de eventos no tiene forma de saber que se está realizando una llamada de bloqueo en algún código síncrono. Eso significa que se supone que no debes usar un sincronizador
open
dentro del contexto de una corrutina. En su lugar, utilice una biblioteca dedicada como archivos ai que proporcione una versión asincrónica deopen
.fuente
yield from
realmente no dice nada.yield from
es solo una construcción de sintaxis, no es un bloque de construcción fundamental que las computadoras puedan ejecutar. Del mismo modo, para select loop. Sí, las corrutinas en Go también usan el bucle de selección, pero lo que estaba tratando de hacer funcionaría en Go, pero no en Python. Necesito respuestas más detalladas para comprender por qué no funcionó.asyncio
hace, para mí, se reduciría al código C que ilustra a qué se tradujo la sintaxis de Python.