Descripción general de Async IO en Python 3.7

D

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.

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad