Descripción general de Async IO en Python 3.7

El asynciomódulo de Python 3 proporciona herramientas fundamentales para implementar E / S asíncronas en Python. Se introdujo en Python 3.4, y con cada versión secundaria posterior, el módulo ha evolucionado significativamente.
Este tutorial contiene una descripción general del paradigma asíncrono y cómo se implementa en Python 3.7.

Bloqueo vs E / S sin bloqueo

El problema que trata de resolver la asincronía está bloqueando E / S .
De forma predeterminada, cuando su programa accede a los datos desde una fuente de E / S, espera a que esa operación se complete antes de continuar ejecutando el 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 está bloqueado de continuar su flujo de ejecución, mientras que se accede a un dispositivo físico, y los datos se transfieren.
Las operaciones de red son otra fuente común de bloqueo:
# pip install --user requests
import requests

req = requests.get('https://www.stackabuse.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 se escala muy mal. Si tiene que esperar a que 10 10 lectura de ficheros o las transacciones de red, el rendimiento se verá afectado.

Multiprocesamiento, subprocesos y asincronía.

Las estrategias para minimizar los retrasos de bloqueo de E / S se dividen en tres categorías principales: multiprocesamiento, subprocesos 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, incluida una porción de memoria (montón) asignada de forma independiente.
Python implementa el paralelismo con el multiprocessingmódulo.
El siguiente es un ejemplo de un programa Python 3 que genera cuatro procesos secundarios, cada uno de los cuales presenta 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()}\t\tProcess {num}, PID: {os.getpid()}, Delay: {delay} seconds...")
    time.sleep(delay)
    print(f"{status()}\t\tProcess {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.  

Enhebrado

El subprocesamiento es una alternativa al multiprocesamiento, con beneficios 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 completamente en un solo proceso del kernel y comparten un único montón asignado.
Los hilos de Python son concurrentes : se ejecutan múltiples secuencias de código de máquina en marcos de tiempo superpuestos. Pero no son paralelos : la ejecución no se produce simultáneamente en varios núcleos físicos.
Las desventajas principales de los hilos de Python son la seguridad de la memoria y las condiciones de carrera . Todos los subprocesos secundarios de un proceso principal funcionan en el mismo espacio de memoria compartida. Sin protecciones adicionales, un subproceso puede sobrescribir un valor compartido en la memoria sin que otros subprocesos lo conozcan. Dicha corrupción de datos sería desastrosa.
Para hacer cumplir la seguridad de subprocesos, las implementaciones de CPython utilizan un bloqueo de intérprete global (GIL). La 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 un hilo se ejecuta en un momento dado.
Aquí está la versión de subproceso del ejemplo de multiprocesamiento de la sección anterior. Note que muy poco ha cambiado: multiprocessing.Processse reemplaza por threading.ThreadComo se indica en la salida, 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()}\t\tProcess {num}, PID: {os.getpid()}, Delay: {delay} seconds...")
    time.sleep(delay)
    print(f"{status()}\t\tProcess {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 a los subprocesos para escribir aplicaciones concurrentes. Los eventos asíncronos ocurren en programaciones independientes, "desincronizadas" unas con otras, completamente dentro de un solo hilo .
A diferencia de los subprocesos, en los programas asíncronos el programador controla cuándo y cómo se produce la preferencia voluntaria, lo que facilita aislar y evitar las condiciones de carrera.

Introducción al módulo asyncio de Python 3.7

En Python 3.7, las operaciones asíncronas son proporcionadas por el asynciomódulo.
API de alto nivel vs nivel bajo de asyncio
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 programa puede escribirse utilizando solo las API de alto nivel. Si no estás escribiendo un marco o biblioteca, nunca necesitas tocar las cosas de bajo nivel.
Dicho esto, echemos un vistazo a las API básicas de alto nivel y analicemos los conceptos básicos.
Coroutines
En general, una corutina (abreviatura de subrutina cooperativa ) es una función diseñada para realizar multitareas preventivas voluntarias : cede de forma proactiva a otras rutinas y procesos, en lugar de ser forzada por el núcleo. El término "coroutine" fue acuñado en 1958 por Melvin Conway (de la fama de "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 espera .
Awaitables, async, y espera
Cualquier objeto que pueda ser esperado (voluntariamente sustituido por una coroutina) se llama aguardable .
La awaitpalabra clave suspende la ejecución de la coroutine actual y llama a la lista de espera especificada.
En Python 3.7, los tres objetos son awaitable coroutinetaskfuture.
Un asyncio coroutinees cualquier función de Python cuya definición es prefijada con la asyncpalabra clave.
async def my_coro():  
    pass
Un asyncio taskes un objeto que envuelve una coroutine, que proporciona métodos para controlar su ejecución y consultar su estado. Una tarea puede ser creada con asyncio.create_task(), o asyncio.gather().
Un asyncio futurees un objeto de bajo nivel que actúa como un 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 adelante, 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 a future, por lo que en Python 3.7 nunca necesita crear un futureobjeto directamente.
Loops de eventos
En asyncio, un bucle de eventos controla la programación y la comunicación de los objetos que se pueden esperar. Se requiere un bucle de eventos para usar aguardientes. Cada programa asyncio tiene al menos un bucle de eventos. Es posible tener múltiples bucles de eventos, pero en Python 3.7 se desaconseja el uso de múltiples bucles de eventos .
Una referencia al objeto de bucle actualmente en ejecución se obtiene llamando asyncio.get_running_loop().
Dormido
El asyncio.sleep(delay)coroutine se bloquea por unos delaysegundos. 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())  
Iniciar el bucle del evento principal
El punto de entrada canónico a un programa de asyncio es asyncio.run(main()), donde se main()encuentra una coroutina 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())  
La llamada asyncio.run()implícitamente crea y ejecuta un bucle de eventos. El objeto de bucle tiene muchos métodos útiles, incluido loop.time(), que devuelve un flotante que representa la hora actual, según lo mide el reloj interno del bucle.
Nota : asyncio.run()No se puede llamar a la función desde 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 bucle 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 por 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 esperable que envuelve una coroutina. 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 rutina tan pronto como pueda".
Si espera una tarea, la ejecución de la rutina actual se bloquea hasta que se complete la 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 manejar la coroutina envuelta. En particular, puede solicitar que se cancele una tarea llamando al .cancel()método de la tarea La tarea se programará para su cancelación en el siguiente ciclo del ciclo de eventos. La cancelación no está garantizada: la tarea puede completarse antes de ese ciclo, en cuyo caso no se produce la cancelación.
Recogiendo Awaitables
Los Awaitables se pueden reunir como un grupo, proporcionándolos como un argumento de lista para la rutina integrada asyncio.gather(awaitables).
Las asyncio.gather()devoluciones se pueden representar de forma esperada en los objetos recopilados y, por lo tanto, se deben prefijar con await.
Si algún elemento de los pendientes es una rutina, se programa de inmediato como una tarea.
La recopilación es una forma conveniente de programar varias coroutinas para que se ejecuten simultáneamente como tareas. También asocia las tareas reunidas de alguna manera útil:
  • Cuando se completan todas las tareas reunidas, sus valores de retorno agregados se devuelven como una lista, ordenados de acuerdo con el orden de lista de pendientes .
  • Cualquier tarea reunida puede ser cancelada, sin cancelar las otras tareas.
  • La recopilación en sí puede ser cancelada, cancelando todas las tareas.
Ejemplo: Async Web Requests 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 asyncio de Scott Robinson . Su programa aprovecha el aiohttpmódulo para agarrar las publicaciones principales en Reddit y enviarlas a la consola.
Asegúrese de tener el aiohttpmódulo instalado antes de ejecutar el script a continuación. Puedes descargar el módulo a través del 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 + '\n\t\t(' + 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 el orden de la salida cambia. 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 la 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:
  • stream , un conjunto de primitivas de red de alto nivel para gestionar eventos TCP asíncronos.
  • bloqueo , evento , condición , análogos asíncronos de las primitivas de sincronización proporcionadas en el módulo de subprocesamiento .
  • subproceso , un conjunto de herramientas para ejecutar subprocesos asíncronos, como comandos de shell.
  • cola , un análogo asíncrono del módulo de cola .
  • excepción , para el manejo de excepciones en código asíncrono.

Conclusión

Tenga en cuenta que incluso si su programa no requiere asincronía por razones de rendimiento, puede usarlo asynciosi prefiere escribir dentro del paradigma asíncrono. Espero que esta descripción general le brinde una comprensión sólida de cómo, cuándo y por qué comenzar a usar el uso asyncio.

Acerca de: Programator

Somos Instinto Programador

0 comentarios:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

Con tecnología de Blogger.