Introducción a asyncio

asyncio es un módulo de python que forma parte de su librería standard. Ya en python 3.6 ha dejado de ser provisional y se le considera estable. La documentación está aquí: https://docs.python.org/3/library/asyncio.html

Este modulo ayuda a hacer programación concurrente de un sólo thread en python, cosa que nunca ha sido el fuerte de este lenguaje de programación. Yo no tengo experiencia con la programación concurrente más que lo mal-aprendido con el javascript de siempre (parece que el nuevo ECMA será más apropiado para la programación concurrente), y alguna que otra cosa aprendida en un curso de métodos numéricos, en los que tampoco se vio en profundidad el tema desde el punto de vista de ciencias computacionales. En el mundo javascript es tan sencillo poner callbacks por todos lados que el código termina siendo difícil de comprender o más complejo de lo que debería ser. Lo que muchos programadores quejosos llaman el callback hell. Tienen razón, aunque tampoco es para tanto. De hecho fue uno de los problemas que tuve con mi proyecto de selfeed. También fue en javascript que me topé con los futuros y las promesas, que usualmente son implementados con javascript y ni siquiera eran parte del standard, sino hasta ahora.

Desde mi inexperiencia e ignorancia con respecto a este tema en python, he ido aprendiendo en los últimos días sobre este módulo y tras leer la documentación y cometer varios errores, he logrado armar un ejemplo bastante básico que presento en este tutorial introductorio.

 

Vamos al código:

# Importando dependencias:

import asyncio

from threading import Thread



# -------------------------------------

# Dos variables globales que seran mutadas por funciones asíncronas



c1 = 0

c2 = 0



# Esto le indica a los loops infinitos que deben detenerse:



stop = False



# -------------------------------------

# Estas dos funciones incrementan las variables globales que se inicializaron arriba:



def change1(x):

    global c1

    c1 += x

    print("Updated C1 ", c1)

    

def change2(x):

    global c2

    c2 += x

    print("Updated C2 ", c2)



# -------------------------------------

# Estos son los ciclos infinitos que correrán paralelamente en el programa:

# El prefijo "async" indica que son "corutinas" que pueden correr paralelamente,

# cediendo el hilo de ejecución para que otras corutinas puedan ejecutarse.



async def servicio1():

    while not stop:

        await asyncio.sleep(2)

        change1(1)



async def servicio2():

    while not stop:

        await asyncio.sleep(2)

        change2(1)



# -------------------------------------

# Esto es un objeto loop, que es la  base del módulo asyncio:

# Se genera un nuevo loop ya que este correrá en otro thread, aunque parece que 

# esto no es necesario y que se puede usar sencillamente get_event_loop, para 

# obtener el loop por defecto del programa principal.

other_thread = asyncio.new_event_loop()



# -------------------------------------

# Esta función inicializa el loop de asyncio y lo ejecuta indefinidamente:

# Esta función será ejecutada dentro de otro thread, por lo que se debe llamar a

# set_event_loop.



def start_loop(_loop):

    asyncio.set_event_loop(_loop)

    _loop.run_forever()

    

# -------------------------------------

# Se genera un nuevo hilo de ejecución. Ya mencioné que asyncio maneja 

# concurrencia en un sólo thread, pero en mi caso, tuve que hacer esto para 

# experimentar en la consola de python. Al correr el loop en el thread principal

# la consola de python, obviamente, se bloquea, con lo que no podía experimentar

# en tiempo real.



t = Thread(target=start_loop, args=(other_thread,))

t.start()



# -------------------------------------

# Como nuestro loop va a correr en otro thread, se debe utilizar los métodos

# que sean "thread safe". Se inician los loops infinitos declarados arriba

# utilizando ensure_future.



other_thread.call_soon_threadsafe(asyncio.ensure_future, servicio1())

other_thread.call_soon_threadsafe(asyncio.ensure_future, servicio2())



# -------------------------------------

# Desde la consola de python se puede detener los loops pseudo-infinitos

# asignando True a la variable stop. Si los procesos no paran, es imposible

# terminar el thread y cerrar el loop de asyncio.



stop = True



# -------------------------------------

# Ahora se puede detener el loop de asyncio, y se puede hacer join con el 

# thread. Notar que la función stop del loop debe llamarse con call_soon_threadsafe. 

# ya que está corriendo en un thread aparte.



other_thread.call_soon_threadsafe(other_thread.stop)

t.join()

other_thread.close()

 

 

Un detalle muy importante es que las funciones con los loops semi-infinitos, servicio1 y servicio2, no permiten la ejecución paralela si no contienen alguna instrucción con “await” o “yield from” ya que es durante estas instrucciones que ocurre la concurrencia y si no se les incluye, en este caso sólo correría el servicio1, que es el primero en iniciar.

En todo esto hay varios conceptos clave que es necesario comprender para saber de qué va todo esto. El objeto loop es un bucle que maneja la ejecución de las tareas que se le pongan. Con call_later o call_at, por ejemplo, se puede programar tareas para alguna cantidad de segundos después, fácilmente y es el loop el que maneja esa planificación. Las tareas pueden ser corutinas asíncronas o pueden ser funciones comunes y hay varias maneras de ejecutarlas. Las corutinas son básicamente futuros, y como tales pueden arrojar errores que pueden manejarse con callbacks y retornar resultados. Al tener un “await” o “yield from” dentro de una corutina, asyncio permite que la cola de tareas pendientes por ejecutar se vayan ejecutando, es decir que estas instrucciones ceden el hilo de ejecución. Algo bueno de utilizar threads y corutinas es que se pueden compartir objetos en el mismo espacio de memoria fácilmente, aunque se tendrá que tener cuidado de no generar condiciones de carrera usando candados, colas o semáforos.

La mayoría de ejemplos que encontré lo hacían todo en el thread principal del programa, cosa que no está mal pero que no me permitía explorar los objetos y la manera en que las cosas se ejecutan desde la consola. Con este código de ejemplo pueden experimentar en la consola de python tranquilamente sin que el loop la bloquee.