Esta pregunta está motivada por mi otra pregunta: ¿Cómo esperar en cdef?
Hay toneladas de artículos y publicaciones de blog en la web sobre asyncio , pero todos son muy superficiales. No pude encontrar ninguna información sobre cómo se implementa realmente asyncio y qué hace que la E/S sea asíncrona. Estaba tratando de leer el código fuente, pero son miles de líneas de código C que no es del más alto grado, muchas de las cuales tratan con objetos auxiliares, pero lo más importante es que es difícil conectar entre la sintaxis de Python y el código C que traduciría. dentro.
La propia documentación de Asyncio es aún menos útil. No hay información allí sobre cómo funciona, solo algunas pautas sobre cómo usarlo, que a veces también son engañosas/muy mal escritas.
Estoy familiarizado con la implementación de rutinas de Go, y esperaba que Python hiciera lo mismo. Si ese fuera el caso, el código que encontré en la publicación vinculada anteriormente habría funcionado. Como no lo hizo, ahora estoy tratando de averiguar por qué. Mi mejor conjetura hasta ahora es la siguiente, corríjame donde me equivoque:
async def foo(): ... en realidad se interpretan como métodos de una clase que hereda coroutine .async def en realidad se divide en varios métodos mediante declaraciones de await , donde el objeto, en el que se llama a estos métodos, puede realizar un seguimiento del progreso realizado a través de la ejecución hasta el momento.await declaración). En otras palabras, aquí está mi intento de "desazucarar" alguna sintaxis asyncio 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 aparte? ¿Se suspende 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, cediendo el control, ¿cómo sabe esto el intérprete de Python y puede continuar ejecutando otro código, mientras que el código del kernel hace el I/ real? O y hasta que despierte el procedimiento de Python que envió la interrupción originalmente? ¿Cómo puede el intérprete de Python, en principio, ser consciente de que esto sucede?
El desazúcar de tu coro es conceptualmente correcto, pero ligeramente incompleto.
await no suspende incondicionalmente, pero solo si encuentra una llamada de bloqueo. ¿Cómo sabe que una llamada está bloqueando? Esto se decide por el código que se espera. Por ejemplo, una implementación esperable de lectura de socket podría reducirse a:
def read(sock, n): # sock must be in non-blocking mode try: return sock.recv(n) except EWOULDBLOCK: event_loop.add_reader(sock.fileno, current_task()) return SUSPEND En asyncio real, el código equivalente modifica el estado de un Future en lugar de devolver valores mágicos, pero el concepto es el mismo. Cuando se adapte adecuadamente a un objeto similar a un generador, se puede await el código anterior.
En el lado de la persona que llama, cuando su rutina contiene:
data = await read(sock, 1024)Se desazucara en algo parecido a:
data = read(sock, 1024) if data is SUSPEND: return SUSPEND self.pos += 1 self.parts[self.pos](...) Las personas familiarizadas con los generadores tienden a describir lo anterior en términos de yield from cual se produce la suspensión de forma automática.
La cadena de suspensión continúa hasta el bucle de eventos, que detecta que la rutina está suspendida, la elimina del conjunto ejecutable y ejecuta las rutinas ejecutables, si las hay. Si no se pueden ejecutar corrutinas, el ciclo espera en select() hasta que un descriptor de archivo en el que está interesada una corrutina esté listo para IO o expire el tiempo de espera. (El bucle de eventos mantiene una asignación de descriptor de archivo a corrutina).
En el ejemplo anterior, una vez que select() le dice al ciclo de eventos que sock es legible, volverá a agregar coro 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 (por lo general, una llamada de E/S que normalmente se bloquearía o un tiempo de espera) esté lista.
Para obtener información sobre los bucles de eventos que impulsan la corrutina, recomiendo esta charla de Dave Beazley, donde demuestra cómo codificar un bucle de eventos desde cero frente a una audiencia en vivo.
Le permite escribir código asíncrono de subproceso único e implementar la concurrencia en Python. Básicamente, asyncio proporciona un bucle de eventos para la programación asincrónica. Por ejemplo, si necesitamos realizar solicitudes sin bloquear el hilo principal, podemos usar la biblioteca asyncio .
El módulo asyncio permite la implementación de programación asíncrona utilizando una combinación de los siguientes elementos:
Bucle de eventos: El módulo asyncio permite un bucle de eventos por proceso.
Corrutinas: Una corrutina es un generador que sigue ciertas convenciones. Su característica más interesante es que puede suspenderse durante la ejecución para esperar el procesamiento externo (alguna rutina en E/S) y regresar desde el punto en que se detuvo cuando se realizó el procesamiento externo.
Futuros: Los futuros representan un proceso que aún no ha terminado. Un futuro es un objeto que se supone que tiene un resultado en el futuro y representa tareas incompletas.
Tareas: esta es una subclase de asyncio .Future que encapsula y administra rutinas. Podemos usar el objeto asyncio.Task para encapsular una rutina.
El concepto más importante dentro de asyncio es el bucle de eventos. Un bucle de eventos le permite escribir código asincrónico utilizando devoluciones de llamada o corrutinas. Las claves para comprender asyncio son los términos de las rutinas y el bucle de eventos. Las corrutinas son funciones con estado cuya ejecución se puede detener mientras se ejecuta otra operación de E/S. Se utiliza un bucle de eventos para orquestar la ejecución de las corrutinas.
Para ejecutar cualquier función de rutina, necesitamos obtener un bucle de eventos. Podemos hacer esto con
loop = asyncio.get_event_loop() Esto nos da un objeto BaseEventLoop . Esto tiene un método run_until_complete que toma una rutina y la ejecuta hasta que finaliza. Luego, la rutina devuelve un resultado. En un nivel bajo, un bucle de eventos ejecuta el BaseEventLoop.rununtilcomplete(future) .