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 ​​en 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 *