Una introducción a la programación asincrónica en Python

Hola. Preparó la traducción de un interesante artículo en vísperas del inicio del curso básico "Python Developer" .








Introducción



La programación asincrónica es un tipo de programación paralela en la que una unidad de trabajo se puede realizar por separado del hilo principal de ejecución de la aplicación. Cuando se completa el trabajo, se notifica al subproceso principal la finalización del flujo de trabajo o se produce un error. Este enfoque tiene muchos beneficios, como un mejor rendimiento de la aplicación y una mejor capacidad de respuesta.







En los últimos años, la programación asincrónica ha atraído mucha atención, y hay razones para esto. Aunque este tipo de programación puede ser más complicado que la ejecución secuencial tradicional, es mucho más eficiente.



Por ejemplo, en lugar de esperar a que se complete la solicitud HTTP antes de continuar con la ejecución, puede enviar la solicitud y hacer otro trabajo que esté esperando en línea usando las rutinas asincrónicas en Python.



La asincronía es una de las principales razones para elegir Node.js para implementar el back-end. Gran parte del código que escribimos, especialmente en aplicaciones pesadas de E / S como sitios web, depende de recursos externos. Puede contener cualquier cosa, desde una llamada a una base de datos remota hasta solicitudes POST a un servicio REST. Una vez que envíe una solicitud a uno de estos recursos, su código simplemente esperará una respuesta. Con la programación asincrónica, deja que su código maneje otras tareas mientras espera una respuesta de los recursos.



¿Cómo logra Python hacer varias cosas al mismo tiempo?







1. Múltiples procesos



La forma más obvia es usar múltiples procesos. Desde el terminal, puede ejecutar su script dos, tres, cuatro, diez veces, y todos los scripts se ejecutarán de forma independiente y simultánea. El sistema operativo se encargará de la distribución de los recursos del procesador entre todas las instancias. Alternativamente, puede usar la biblioteca de multiprocesamiento , que puede generar múltiples procesos, como se muestra en el siguiente ejemplo.



from multiprocessing import Process

def print_func(continent='Asia'):
    print('The name of continent is : ', continent)

if __name__ == "__main__":  # confirms that the code is under main function
    names = ['America', 'Europe', 'Africa']
    procs = []
    proc = Process(target=print_func)  # instantiating without any argument
    procs.append(proc)
    proc.start()

    # instantiating process with arguments
    for name in names:
        # print(name)
        proc = Process(target=print_func, args=(name,))
        procs.append(proc)
        proc.start()

    # complete the processes
    for proc in procs:
        proc.join()


Salida:



The name of continent is :  Asia
The name of continent is :  America
The name of continent is :  Europe
The name of continent is :  Africa


2. Varios subprocesos



Otra forma de ejecutar varios trabajos en paralelo es usar subprocesos. Un subproceso es una cola de ejecución, que es muy similar a un proceso, sin embargo, puede tener varios subprocesos en un solo proceso, y todos ellos compartirán recursos. Sin embargo, esto dificultará la escritura del código de transmisión. Del mismo modo, el sistema operativo hará todo el trabajo duro de asignar memoria del procesador, pero un bloqueo de intérprete global (GIL) solo permitirá que se ejecute un hilo de Python a la vez, incluso si tiene un código multiproceso. Así es como el GIL en CPython evita la concurrencia de múltiples núcleos. Es decir, puede ejecutar a la fuerza solo un núcleo, incluso si tiene dos, cuatro o más.



import threading
 
def print_cube(num):
    """
    function to print cube of given num
    """
    print("Cube: {}".format(num * num * num))
 
def print_square(num):
    """
    function to print square of given num
    """
    print("Square: {}".format(num * num))
 
if __name__ == "__main__":
    # creating thread
    t1 = threading.Thread(target=print_square, args=(10,))
    t2 = threading.Thread(target=print_cube, args=(10,))
 
    # starting thread 1
    t1.start()
    # starting thread 2
    t2.start()
 
    # wait until thread 1 is completely executed
    t1.join()
    # wait until thread 2 is completely executed
    t2.join()
 
    # both threads completely executed
    print("Done!")


Salida:



Square: 100
Cube: 1000
Done!


3. Corutinas y yield: Las



corutinas son una generalización de subrutinas. Se utilizan para la multitarea cooperativa, cuando un proceso entrega voluntariamente el control ( yield) con cierta frecuencia o durante períodos de espera para permitir que se ejecuten varias aplicaciones al mismo tiempo. Las corutinas son similares a los generadores , pero con métodos adicionales y cambios menores en la forma en que usamos la declaración de rendimiento . Los generadores producen datos para la iteración, mientras que las corutinas también pueden consumir datos.



def print_name(prefix):
    print("Searching prefix:{}".format(prefix))
    try : 
        while True:
                # yeild used to create coroutine
                name = (yield)
                if prefix in name:
                    print(name)
    except GeneratorExit:
            print("Closing coroutine!!")
 
corou = print_name("Dear")
corou.__next__()
corou.send("James")
corou.send("Dear James")
corou.close()


Salida:



Searching prefix:Dear
Dear James
Closing coroutine!!


4. Programación asincrónica



El cuarto método es la programación asincrónica, en la cual el sistema operativo no está involucrado. En el lado del sistema operativo, tendrá un proceso en el que solo habrá un subproceso, pero aún puede realizar varias tareas al mismo tiempo. Entonces, ¿cuál es el truco?



Respuesta: asyncio



Asyncio- El módulo de programación asíncrono que se introdujo en Python 3.4. Está diseñado para usar corutinas y futuros para que sea más fácil escribir código asincrónico y lo hace casi tan legible como el código síncrono debido a la falta de devoluciones de llamada.



Asyncioutiliza diferentes construcciones: event loopcorutinas y future.



  • event loop . .
  • ( ) – , Python, await event loop. event loop. Tasks, Future.
  • Future , . exception.


Con ayuda, asynciopuede estructurar su código para que las subtareas se definan como corutinas y le permita programarlas para que se ejecuten a su gusto, incluso al mismo tiempo. Las rutinas contienen puntos yielden los que definimos posibles puntos de cambio de contexto. Si hay tareas en la cola de espera, el contexto cambiará, de lo contrario no lo hará.



Un cambio de contexto asyncioes event loop, que transfiere el flujo de control de una rutina a otra.



En el siguiente ejemplo, ejecutamos 3 tareas asincrónicas que individualmente realizan solicitudes a Reddit, recuperan y generan contenido JSON. Usamos aiohttp - La biblioteca de cliente http, que garantiza que incluso una solicitud HTTP se ejecutará de forma asincrónica.



import signal  
import sys  
import asyncio  
import aiohttp  
import json

loop = asyncio.get_event_loop()  
client = aiohttp.ClientSession(loop=loop)

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):  
    data1 = await get_json(client, 'https://www.reddit.com/r/' + subreddit + '/top.json?sort=top&t=day&limit=5')

    j = json.loads(data1.decode('utf-8'))
    for i in j['data']['children']:
        score = i['data']['score']
        title = i['data']['title']
        link = i['data']['url']
        print(str(score) + ': ' + title + ' (' + link + ')')

    print('DONE:', subreddit + '\n')

def signal_handler(signal, frame):  
    loop.stop()
    client.close()
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)

asyncio.ensure_future(get_reddit_top('python', client))  
asyncio.ensure_future(get_reddit_top('programming', client))  
asyncio.ensure_future(get_reddit_top('compsci', client))  
loop.run_forever()


Salida:



50: Undershoot: Parsing theory in 1965 (http://jeffreykegler.github.io/Ocean-of-Awareness-blog/individual/2018/07/knuth_1965_2.html)
12: Question about best-prefix/failure function/primal match table in kmp algorithm (https://www.reddit.com/r/compsci/comments/8xd3m2/question_about_bestprefixfailure_functionprimal/)
1: Question regarding calculating the probability of failure of a RAID system (https://www.reddit.com/r/compsci/comments/8xbkk2/question_regarding_calculating_the_probability_of/)
DONE: compsci

336: /r/thanosdidnothingwrong -- banning people with python (https://clips.twitch.tv/AstutePluckyCocoaLitty)
175: PythonRobotics: Python sample codes for robotics algorithms (https://atsushisakai.github.io/PythonRobotics/)
23: Python and Flask Tutorial in VS Code (https://code.visualstudio.com/docs/python/tutorial-flask)
17: Started a new blog on Celery - what would you like to read about? (https://www.python-celery.com)
14: A Simple Anomaly Detection Algorithm in Python (https://medium.com/@mathmare_/pyng-a-simple-anomaly-detection-algorithm-2f355d7dc054)
DONE: python

1360: git bundle (https://dev.to/gabeguz/git-bundle-2l5o)
1191: Which hashing algorithm is best for uniqueness and speed? Ian Boyd's answer (top voted) is one of the best comments I've seen on Stackexchange. (https://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed)
430: ARM launches “Facts” campaign against RISC-V (https://riscv-basics.com/)
244: Choice of search engine on Android nuked by “Anonymous Coward” (2009) (https://android.googlesource.com/platform/packages/apps/GlobalSearch/+/592150ac00086400415afe936d96f04d3be3ba0c)
209: Exploiting freely accessible WhatsApp data or “Why does WhatsApp web know my phone’s battery level?” (https://medium.com/@juan_cortes/exploiting-freely-accessible-whatsapp-data-or-why-does-whatsapp-know-my-battery-level-ddac224041b4)
DONE: programming


Uso de Redis y Redis Queue RQ



Usar asyncioy aiohttpno siempre es una buena idea, especialmente si está utilizando versiones anteriores de Python. Además, hay momentos en los que necesita distribuir tareas entre diferentes servidores. En este caso, puede usar RQ (Redis Queue). Esta es la biblioteca habitual de Python para agregar trabajos a la cola y procesarlos por los trabajadores en segundo plano. Para organizar la cola, se usa Redis, una base de datos de claves / valores.



En el siguiente ejemplo, agregamos una función simple a la cola count_words_at_urlusando Redis.



from mymodule import count_words_at_url
from redis import Redis
from rq import Queue


q = Queue(connection=Redis())
job = q.enqueue(count_words_at_url, 'http://nvie.com')


******mymodule.py******

import requests

def count_words_at_url(url):
    """Just an example function that's called async."""
    resp = requests.get(url)

    print( len(resp.text.split()))
    return( len(resp.text.split()))


Salida:



15:10:45 RQ worker 'rq:worker:EMPID18030.9865' started, version 0.11.0
15:10:45 *** Listening on default...
15:10:45 Cleaning registries for queue: default
15:10:50 default: mymodule.count_words_at_url('http://nvie.com') (a2b7451e-731f-4f31-9232-2b7e3549051f)
322
15:10:51 default: Job OK (a2b7451e-731f-4f31-9232-2b7e3549051f)
15:10:51 Result is kept for 500 seconds


Conclusión



Como ejemplo, tomemos una exhibición de ajedrez donde uno de los mejores jugadores compite contra una gran cantidad de personas. Tenemos 24 juegos y 24 personas para jugar, y si un jugador de ajedrez juega sincrónicamente con ellos, tomará al menos 12 horas (suponiendo que un juego promedio tome 30 movimientos, el jugador de ajedrez piensa en un movimiento en 5 segundos, y oponente - aproximadamente 55 segundos.) Sin embargo, en el modo asíncrono, el jugador de ajedrez podrá hacer un movimiento y dejar al oponente tiempo para pensar, mientras tanto, pasar al siguiente oponente y dividir el movimiento. Por lo tanto, puedes hacer un movimiento en los 24 juegos en 2 minutos, y todos se pueden ganar en solo una hora.



Esto es lo que implica cuando la gente dice que la asincronía agiliza las cosas. Estamos hablando de tal velocidad. Un buen jugador de ajedrez no comienza a jugar ajedrez más rápido, solo el tiempo está más optimizado y no se pierde en esperar. Así es como funciona.



Según esta analogía, un jugador de ajedrez será un procesador, y la idea principal será mantener el procesador inactivo durante el menor tiempo posible. Se trata de tener siempre algo que hacer.



En la práctica, la asincronía se define como un estilo de programación paralela en el que algunas tareas liberan al procesador durante los períodos de espera para que otras tareas puedan aprovecharlo. Python tiene varias formas de lograr la concurrencia para satisfacer sus necesidades, flujo de código, manejo de datos, arquitectura y casos de uso, y puede elegir entre cualquiera de ellos.






.







All Articles