Descripci贸n general de Async IO en Python 3.7

    Python 3 asyncio El m贸dulo proporciona herramientas fundamentales para implementar E / S asincr贸nicas en Python. Se introdujo en Python 3.4, y con cada versi贸n menor posterior, el m贸dulo ha evolucionado significativamente.

    Este tutorial contiene una descripci贸n general del paradigma asincr贸nico y c贸mo se implementa en Python 3.7.

    E / S con bloqueo frente a sin bloqueo

    El problema que la asincron铆a busca resolver es el bloqueo de E / S.

    De forma predeterminada, cuando su programa accede a datos de una fuente de E / S, espera a que se complete la operaci贸n antes de continuar con la ejecuci贸n del programa.

    with open('myfile.txt', 'r') as file:
        data = file.read()
        # Until the data is read into memory, the program waits here
    print(data)
    

    El programa no puede continuar con su flujo de ejecuci贸n mientras se accede a un dispositivo f铆sico y se transfieren datos.

    Las operaciones de red son otra fuente com煤n de bloqueo:

    # pip install --user requests
    import requests
    
    req = requests.get('https://www.Pharos.sh.com/')
    
    #
    # Blocking occurs here, waiting for completion of an HTTPS request
    #
    
    print(req.text)
    

    En muchos casos, el retraso causado por el bloqueo es insignificante. Sin embargo, el bloqueo de E / S escala muy mal. Si necesita esperar 1010 lecturas de archivos o transacciones de red, el rendimiento se ver谩 afectado.

    Multiprocesamiento, subprocesamiento y asincron铆a

    Las estrategias para minimizar los retrasos del bloqueo de E / S se dividen en tres categor铆as principales: multiprocesamiento, subprocesamiento y asincron铆a.

    Multiprocesamiento

    El multiprocesamiento es una forma de computaci贸n paralela: las instrucciones se ejecutan en un marco de tiempo superpuesto en m煤ltiples procesadores f铆sicos o n煤cleos. Cada proceso generado por el kernel incurre en un costo general, que incluye una porci贸n de memoria asignada de forma independiente (mont贸n).

    Python implementa el paralelismo con el multiprocessing m贸dulo.

    El siguiente es un ejemplo de un programa de Python 3 que genera cuatro procesos secundarios, cada uno de los cuales exhibe un retraso aleatorio e independiente. La salida muestra el ID de proceso de cada hijo, la hora del sistema antes y despu茅s de cada retraso, y la asignaci贸n de memoria actual y m谩xima en cada paso.

    from multiprocessing import Process
    import os, time, datetime, random, tracemalloc
    
    tracemalloc.start()
    children = 4    # number of child processes to spawn
    maxdelay = 6    # maximum delay in seconds
    
    def status():
        return ('Time: ' + 
            str(datetime.datetime.now().time()) +
            't Malloc, Peak: ' +
            str(tracemalloc.get_traced_memory()))
    
    def child(num):
        delay = random.randrange(maxdelay)
        print(f"{status()}ttProcess {num}, PID: {os.getpid()}, Delay: {delay} seconds...")
        time.sleep(delay)
        print(f"{status()}ttProcess {num}: Done.")
    
    if __name__ == '__main__':
        print(f"Parent PID: {os.getpid()}")
        for i in range(children):
            proc = Process(target=child, args=(i,))
            proc.start()
    

    Salida:

    Parent PID: 16048
    Time: 09:52:47.014906    Malloc, Peak: (228400, 240036)     Process 0, PID: 16051, Delay: 1 seconds...
    Time: 09:52:47.016517    Malloc, Peak: (231240, 240036)     Process 1, PID: 16052, Delay: 4 seconds...
    Time: 09:52:47.018786    Malloc, Peak: (231616, 240036)     Process 2, PID: 16053, Delay: 3 seconds...
    Time: 09:52:47.019398    Malloc, Peak: (232264, 240036)     Process 3, PID: 16054, Delay: 2 seconds...
    Time: 09:52:48.017104    Malloc, Peak: (228434, 240036)     Process 0: Done.
    Time: 09:52:49.021636    Malloc, Peak: (232298, 240036)     Process 3: Done.
    Time: 09:52:50.022087    Malloc, Peak: (231650, 240036)     Process 2: Done.
    Time: 09:52:51.020856    Malloc, Peak: (231274, 240036)     Process 1: Done.
    

    Enhebrar

    El enhebrado es una alternativa al multiprocesamiento, con ventajas y desventajas.

    Los subprocesos se programan de forma independiente y su ejecuci贸n puede ocurrir dentro de un per铆odo de tiempo superpuesto. Sin embargo, a diferencia del multiprocesamiento, los subprocesos existen por completo en un solo proceso de kernel y comparten un solo mont贸n asignado.

    Los subprocesos de Python son concurrentes: se ejecutan m煤ltiples secuencias de c贸digo de m谩quina en per铆odos de tiempo superpuestos. Pero no son en paralelo: la ejecuci贸n no se produce simult谩neamente en varios n煤cleos f铆sicos.

    Las principales desventajas del subproceso de Python son la seguridad de la memoria y las condiciones de carrera. Todos los subprocesos secundarios de un proceso principal operan en el mismo espacio de memoria compartida. Sin protecciones adicionales, un hilo puede sobrescribir un valor compartido en la memoria sin que otros hilos lo sepan. Tal corrupci贸n de datos ser铆a desastrosa.

    Para hacer cumplir la seguridad de los subprocesos, CPython las implementaciones utilizan un bloqueo de int茅rprete global (GIL). GIL es un mecanismo de exclusi贸n mutua que evita que varios subprocesos se ejecuten simult谩neamente en objetos de Python. Efectivamente, esto significa que solo se ejecuta un hilo en un momento dado.

    Aqu铆 est谩 la versi贸n enhebrada del ejemplo de multiprocesamiento de la secci贸n anterior. Tenga en cuenta que muy poco ha cambiado: multiprocessing.Process es reemplazado por threading.Thread. Como se indica en el resultado, todo sucede en un solo proceso y la huella de memoria es significativamente menor.

    from threading import Thread
    import os, time, datetime, random, tracemalloc
    
    tracemalloc.start()
    children = 4    # number of child threads to spawn
    maxdelay = 6    # maximum delay in seconds
    
    def status():
        return ('Time: ' + 
            str(datetime.datetime.now().time()) +
            't Malloc, Peak: ' +
            str(tracemalloc.get_traced_memory()))
    
    def child(num):
        delay = random.randrange(maxdelay)
        print(f"{status()}ttProcess {num}, PID: {os.getpid()}, Delay: {delay} seconds...")
        time.sleep(delay)
        print(f"{status()}ttProcess {num}: Done.")
    
    if __name__ == '__main__':
        print(f"Parent PID: {os.getpid()}")
        for i in range(children):
            thr = Thread(target=child, args=(i,))
            thr.start()
    

    Salida:

    Parent PID: 19770
    Time: 10:44:40.942558    Malloc, Peak: (9150, 9264)     Process 0, PID: 19770, Delay: 3 seconds...
    Time: 10:44:40.942937    Malloc, Peak: (13989, 14103)       Process 1, PID: 19770, Delay: 5 seconds...
    Time: 10:44:40.943298    Malloc, Peak: (18734, 18848)       Process 2, PID: 19770, Delay: 3 seconds...
    Time: 10:44:40.943746    Malloc, Peak: (23959, 24073)       Process 3, PID: 19770, Delay: 2 seconds...
    Time: 10:44:42.945896    Malloc, Peak: (26599, 26713)       Process 3: Done.
    Time: 10:44:43.945739    Malloc, Peak: (26741, 27223)       Process 0: Done.
    Time: 10:44:43.945942    Malloc, Peak: (26851, 27333)       Process 2: Done.
    Time: 10:44:45.948107    Malloc, Peak: (24639, 27475)       Process 1: Done.
    

    Asincron铆a

    La asincron铆a es una alternativa al subproceso para escribir aplicaciones simult谩neas. Los eventos asincr贸nicos ocurren en horarios independientes, “desincronizados” entre s铆, completamente dentro de un solo hilo.

    A diferencia del subproceso, en los programas asincr贸nicos el programador controla cu谩ndo y c贸mo se produce la preferencia voluntaria, lo que facilita el aislamiento y la evitaci贸n de condiciones de carrera.

    Introducci贸n al m贸dulo asyncio de Python 3.7

    En Python 3.7, las operaciones asincr贸nicas las proporciona el asyncio m贸dulo.

    API asyncio de alto nivel frente a bajo nivel

    Los componentes de Asyncio se dividen en API de alto nivel (para escribir programas) y API de bajo nivel (para escribir bibliotecas o marcos basados 鈥嬧媏n asyncio).

    Cada asyncio El programa se puede escribir utilizando solo las API de alto nivel. Si no est谩 escribiendo un marco o una biblioteca, nunca necesita tocar las cosas de bajo nivel.

    Dicho esto, veamos las API principales de alto nivel y analicemos los conceptos b谩sicos.

    Corutinas

    En general, una corrutina (abreviatura de subrutina cooperativa) es una funci贸n dise帽ada para la multitarea preventiva voluntaria: cede proactivamente a otras rutinas y procesos, en lugar de ser reemplazada por el kernel. El t茅rmino “corrutina” fue acu帽ado en 1958 por Melvin Conway (famoso por la “Ley de Conway”), para describir el c贸digo que facilita activamente las necesidades de otras partes de un sistema.

    En asyncio, esta preferencia voluntaria se llama en espera.

    Awaitables, Async y Await

    Cualquier objeto que pueda ser esperado (sustituido voluntariamente por una corrutina) se denomina esperado.

    los await La palabra clave suspende la ejecuci贸n de la corrutina actual y llama al awaitable especificado.

    En Python 3.7, los tres objetos en espera son coroutine, tasky future.

    Un asyncio coroutine es cualquier funci贸n de Python cuya definici贸n tenga el prefijo async palabra clave.

    async def my_coro():
        pass
    

    Un asyncio task es un objeto que envuelve una corrutina, proporcionando m茅todos para controlar su ejecuci贸n y consultar su estado. Se puede crear una tarea con asyncio.create_task()o asyncio.gather().

    Un asyncio future es un objeto de bajo nivel que act煤a como marcador de posici贸n para los datos que a煤n no se han calculado o recuperado. Puede proporcionar una estructura vac铆a para rellenar con datos m谩s tarde y un mecanismo de devoluci贸n de llamada que se activa cuando los datos est谩n listos.

    Una tarea hereda todos menos dos de los m茅todos disponibles para un future, por lo que en Python 3.7 nunca es necesario crear un future objeto directamente.

    Bucles de eventos

    En asyncio, un bucle de eventos controla la programaci贸n y comunicaci贸n de los objetos en espera. Se requiere un bucle de eventos para usar esperables. Cada programa asyncio tiene al menos un ciclo de eventos. Es posible tener m煤ltiples bucles de eventos, pero se desaconseja enf谩ticamente que haya m煤ltiples bucles de eventos en Python 3.7.

    Se obtiene una referencia al objeto de bucle que se est谩 ejecutando actualmente llamando asyncio.get_running_loop().

    Dormido

    los asyncio.sleep(delay) bloques de rutina para delay segundos. Es 煤til para simular el bloqueo de E / S.

    import asyncio
    
    async def main():
        print("Sleep now.")
        await asyncio.sleep(1.5)
        print("OK, wake up!")
    
    asyncio.run(main())
    
    Inicio del bucle de evento principal

    El punto de entrada can贸nico a un programa asyncio es asyncio.run(main()), d贸nde main() es una corrutina de nivel superior.

    import asyncio
    
    async def my_coro(arg):
        "A coroutine."  
        print(arg)
    
    async def main():
        "The top-level coroutine."
        await my_coro(42)
    
    asyncio.run(main())
    

    Vocaci贸n asyncio.run() crea y ejecuta impl铆citamente un bucle de eventos. El objeto de bucle tiene muchos m茅todos 煤tiles, incluidos loop.time(), que devuelve un flotador que representa la hora actual, medida por el reloj interno del bucle.

    Nota: Los asyncio.run() La funci贸n no se puede llamar desde dentro de un bucle de eventos existente. Por lo tanto, es posible que vea errores si est谩 ejecutando el programa dentro de un entorno de supervisi贸n, como Anaconda o Jupyter, que est谩 ejecutando un ciclo de eventos propio. Los programas de ejemplo en esta secci贸n y las siguientes secciones deben ejecutarse directamente desde la l铆nea de comandos ejecutando el archivo python.

    El siguiente programa imprime l铆neas de texto, bloqueando durante un segundo despu茅s de cada l铆nea hasta la 煤ltima.

    import asyncio
    
    async def my_coro(delay):
        loop = asyncio.get_running_loop()
        end_time = loop.time() + delay
        while True:
            print("Blocking...")
            await asyncio.sleep(1)
            if loop.time() > end_time:
                print("Done.")
                break
    
    async def main():
        await my_coro(3.0)
    
    asyncio.run(main())
    

    Salida:

    Blocking...
    Blocking...
    Blocking...
    Done.
    
    Tareas

    Una tarea es un objeto en espera que envuelve una corrutina. Para crear y programar una tarea de inmediato, puede llamar a lo siguiente:

    asyncio.create_task(coro(args...))
    

    Esto devolver谩 un objeto de tarea. La creaci贸n de una tarea le dice al bucle, “siga adelante y ejecute esta corrutina tan pronto como pueda”.

    Si espera una tarea, la ejecuci贸n de la corrutina actual se bloquea hasta que se complete esa tarea.

    import asyncio
    
    async def my_coro(n):
        print(f"The answer is {n}.")
    
    async def main():
        # By creating the task, it's scheduled to run 
        # concurrently, at the event loop's discretion.
        mytask = asyncio.create_task(my_coro(42))
        
        # If we later await the task, execution stops there
        # until the task is complete. If the task is already
        # complete before it is awaited, nothing is awaited. 
        await mytask
    
    asyncio.run(main())
    

    Salida:

    The answer is 42.
    

    Las tareas tienen varios m茅todos 煤tiles para administrar la corrutina envuelta. En particular, puede solicitar que se cancele una tarea llamando al .cancel() m茅todo. La tarea se programar谩 para su cancelaci贸n en el pr贸ximo ciclo del bucle de eventos. La cancelaci贸n no est谩 garantizada: la tarea puede completarse antes de ese ciclo, en cuyo caso la cancelaci贸n no se produce.

    Reuniendo a los esperables

    Los esperables se pueden recopilar como un grupo, proporcion谩ndolos como un argumento de lista para la corrutina incorporada asyncio.gather(awaitables).

    los asyncio.gather() devuelve un awaitable que representa los esperables reunidos y, por lo tanto, debe ir precedido await.

    Si alg煤n elemento de esperables es una corrutina, se programa inmediatamente como una tarea.

    La recopilaci贸n es una forma conveniente de programar m煤ltiples corrutinas para que se ejecuten simult谩neamente como tareas. Tambi茅n asocia las tareas recopiladas de algunas formas 煤tiles:

    • Cuando se completan todas las tareas recopiladas, sus valores de retorno agregados se devuelven como una lista, ordenados de acuerdo con el orden de la lista de esperables.
    • Cualquier tarea recopilada puede cancelarse, sin cancelar las otras tareas.
    • La recopilaci贸n en s铆 se puede cancelar, cancelando todas las tareas.
    Ejemplo: solicitudes web asincr贸nicas con aiohttp

    El siguiente ejemplo ilustra c贸mo se pueden implementar estas API asyncio de alto nivel. La siguiente es una versi贸n modificada, actualizada para Python 3.7, del ingenioso ejemplo de asyncio de Scott Robinson. Su programa aprovecha la aiohttp m贸dulo para tomar las publicaciones superiores en Reddit y enviarlas a la consola.

    Aseg煤rate de tener aiohttp m贸dulo instalado antes de ejecutar el siguiente script. Puede descargar el m贸dulo mediante el siguiente comando pip:

    $ pip install --user aiohttp
    
    import sys  
    import asyncio  
    import aiohttp  
    import json
    import datetime
    
    async def get_json(client, url):  
        async with client.get(url) as response:
            assert response.status == 200
            return await response.read()
    
    async def get_reddit_top(subreddit, client, numposts):  
        data = await get_json(client, 'https://www.reddit.com/r/' + 
            subreddit + '/top.json?sort=top&t=day&limit=" +
            str(numposts))
    
        print(f"n/r/{subreddit}:')
    
        j = json.loads(data.decode('utf-8'))
        for i in j['data']['children']:
            score = i['data']['score']
            title = i['data']['title']
            link = i['data']['url']
            print('t' + str(score) + ': ' + title + 'ntt(' + link + ')')
    
    async def main():
        print(datetime.datetime.now().strftime("%A, %B %d, %I:%M %p"))
        print('---------------------------')
        loop = asyncio.get_running_loop()  
        async with aiohttp.ClientSession(loop=loop) as client:
            await asyncio.gather(
                get_reddit_top('python', client, 3),
                get_reddit_top('programming', client, 4),
                get_reddit_top('asyncio', client, 2),
                get_reddit_top('dailyprogrammer', client, 1)
                )
    
    asyncio.run(main())
    

    Si ejecuta el programa varias veces, ver谩 que cambia el orden de la salida. Esto se debe a que las solicitudes JSON se muestran a medida que se reciben, lo que depende del tiempo de respuesta del servidor y de la latencia de red intermedia. En un sistema Linux, puede observar esto en acci贸n ejecutando el script con el prefijo (por ejemplo) watch -n 5, que actualizar谩 la salida cada 5 segundos:

    Otras API de alto nivel

    Con suerte, esta descripci贸n general le brinda una base s贸lida de c贸mo, cu谩ndo y por qu茅 usar asyncio. Otras API de asyncio de alto nivel, que no se tratan aqu铆, incluyen:

    • corriente, un conjunto de primitivas de red de alto nivel para gestionar eventos TCP asincr贸nicos.
    • bloquear, evento, condici贸n, an谩logos as铆ncronos de las primitivas de sincronizaci贸n proporcionadas en el enhebrar m贸dulo.
    • subproceso, un conjunto de herramientas para ejecutar subprocesos as铆ncronos, como comandos de shell.
    • cola, un an谩logo asincr贸nico del cola m贸dulo.
    • excepci贸n, para manejar excepciones en c贸digo asincr贸nico.

    Conclusi贸n

    Tenga en cuenta que incluso si su programa no requiere asincron铆a por razones de rendimiento, a煤n puede usar asyncio si prefiere escribir dentro del paradigma asincr贸nico. Espero que esta descripci贸n general le brinde una comprensi贸n s贸lida de c贸mo, cu谩ndo y por qu茅 comenzar a usar use asyncio.

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *