Asyncio – Python 3.4

Las entradas referentes a la librería asyncio son meramente apuntes y están hechas en un entorno con Python 3.4 . Por lo que es posible que existan errores de concepto.

Acerca de asyncio

La librería asyncio permite realizar operaciones de forma concurrente en un único hilo de ejecución, para ello proporciona un bucle basado en eventos. Este bucle permite gestionar de una forma eficiente operaciones E/S, cambios de contextos y eventos del sistema. Aunque asyncio proporciona un bucle por defecto desde la misma aplicación pero se pueden utilizar otras implementaciones.

La aplicación que utiliza este bucle registra en él el código a ejecutar, pudiendo determinar en qué momento será puesto en marcha, así como las condiciones bajo las que debe hacerlo. Se pueden registrar funciones propias de Python para ser ejecutadas, o bien las corutinas proporcionadas por asyncio.

Funcionamiento

El funcionamiento de este bucle se basa principalmente en un juego de quién controla el tiempo de ejecución disponible en él, el que por defecto se encuentra ejecutándose en el hilo principal. Esto quiere decir que se puede tener varios hilos de ejecución y en cada uno de ellos un bucle de asyncio realizando operaciones concurrentes.

Cuando el bucle se pone en marcha comienza a ejecutar las funciones registradas, cediéndoles a estas el control y esperando que en algún momento la que se esté ejecutando se lo devuelva, lo que le permitirá ejecutar otras funciones.

Componentes principales

El bucle basado en eventos (event loop)

Asyncio está basado en estos bucles, como se ha mencionado anteriormente este proporciona métodos para gestionar operaciones de E/S, la creación de clientes/servidores de varios tipos de comunicación y el lanzamiento de subprocesos.

Para obtener un bucle se puede hacer de dos formas:

  • asyncio.get_event_loop()
  • asyncio.new_event_loop()

El bucle tiene diferente modos de funcionamiento, que se pueden activar con los siguientes métodos.

  • bucle.run_forever():  Se ejecuta en un modo infinito. Se puede detener con el método stop(). Consultad la documentación, ya que este método no detiene el bucle inmediatamente, hay ciertos aspectos a tener en cuenta dependiendo de si el bucle está en ejecución. Si el bucle todavía NO se está ejecutando se puede utilizar close() que elimina las funciones  registradas en el bucle.
  • bucle.run_until_complete(Future): Ejecutarse hasta que se satisfaga una condición (un Future o derivado) y una vez satisfecha devuelve el resultado o la excepción producida.
    • Nota: Cuando el argumento es una corutina esta es envuelta para ser un Future.

Ejemplo:

import asyncio

# En este bucle no se hace nada.
# Sólo espera una señal para salir
# como control+c
bucle = asyncio.get_event_loop()
bucle.run_forever()

El bucle posee diferentes métodos para interactuar con él, pero los que se utilizan con frecuencia son los siguientes:

  • bucle.call_soon(función, *args): Registra una función/método en el bucle para ser ejecutado tan pronto como sea posible.
  • bucle.call_at(momento, función, *args): Programa una función para ser ejecutada en un momento determinado. Las corutinas no pueden ser llamadas mediante este método. Esta función devuelve un objeto llamado asyncio.Handle que permite cancelar la ejecución de la función incluso después de la puesta en marcha del bucle.
  • bucle.call_later(retraso, función, *args): Programa una función para ser llamada después del tiempo especificado (en segundos). Internamente llama call_at, por lo que no puede ejecutar una corutina. Por lo que si es necesario llamar a una corutina cada X tiempo esta debe ser programada desde una función normal.

Estos métodos permiten registrar funciones normales de Python, pero no se les puede pasar el valor de un argumento específico directamente, por lo que es necesario utilizar functools.partial() para ello.

Ejemplo con functools:

import asyncio
from functools import partial

def coche_info(modelo, matricula):
   msg = "Modelo del coche {0} con matrícula {1}"
   print (msg.format(modelo, matricula))

# Le damos un valor sólo al argumento "matricula" de la
# función.
mi_coche = partial(coche_info, matricula="1234")
bucle = asyncio.get_event_loop()

# Le decimos el modelo del coche, ya no es necesario especificar
# la matrícula.
bucle.call_soon(mi_coche, "VTR 23")
bucle.run_forever()

# Resultado de la ejecución:
# Modelo del coche VTR 23 con matrícula 1234

Ejemplo – Llamada simple

import asyncio

def llamame(bucle):
    print ("Holaa")
    # Detenemos el bucle
    bucle.stop()

# Obtenemos un bucle
bucle = asyncio.get_event_loop()

# Registramos la función llamame y 
# le pasamos como argumento el bucle
bucle.call_soon(llamame, bucle)

# Iniciamos el bucle en modo de 
# ejecutarse para siempre
bucle.run_forever()
# Salida. Ejecuta la función y se mantiene dentro del bucle.
Holaa

Aparte de estos métodos puede resultar interesante saber que internamente asyncio posee un reloj monótono propio para programar la llamada de las funciones, que indica un tiempo diferente al que devuelve time.time(). Este no se ve afectado por cambios en la hora del sistema.

Ejemplo – Mostrando el reloj interno cada 2 segundos

import asyncio

def llamame_luego(bucle):
    print ("Hola, mi reloj interno pone: {0}"
            .format(bucle.time()) )
   
    # Programamos una llamada en 2 segundos
    # a esta función
    bucle.call_at(bucle.time()+2, llamame_luego, bucle)

# Obtenemos un bucle
bucle = asyncio.get_event_loop()

# Registramos la función llamame y 
# le pasamos como argumento el bucle
bucle.call_soon(llamame_luego, bucle)

# Iniciamos el bucle en modo 
# "ejecutarse para siempre"
bucle.run_forever()
# Salida. El tiempo puede ser diferente !
Hola, mi reloj interno pone: 30359.646896135
Hola, mi reloj interno pone: 30361.648696301
Hola, mi reloj interno pone: 30363.65099661

Corutinas

El potencial de las corutinas básicamente es que pueden suspender su propia ejecución mientras espera a que suceda un evento (que finalice la lectura de un archivo, la respuesta de un cliente mediante sockets, etc).

Cuando una corutina suspende su ejecución le está devolviendo el control al bucle en el que está registrado, el cual continua con su ejecución cuando aquello que esperaba ha ocurrido.

La definición de una corutina se puede realizar por medio del decorador:    @asyncio.coroutine

Cuando se trabaja con las corutinas estas pueden suspender su ejecución llamando a: yield from otra_corutina() o Future

Si se utiliza un Future, que se explicará en el siguiente apartado, la función devolverá el control cuando se complete/satisfaga el Future. En caso de que el Future sea cancelado levantará una excepción en la función que lo espera (eso implica que deberás estar preparado para controlarla si en tu código participan acciones que lo podrían cancelar).

Ejemplo de corutina simple:

import asyncio

@asyncio.coroutine
def corutina():
    print ("Espero...")
    yield from asyncio.sleep(3) # Devolvemos el control al bucle
    print ("Ya no espero más")

bucle = asyncio.get_event_loop()
bucle.run_until_complete(corutina())

Al llamar a una corutina esta no se ejecuta sino que devuelve un generador, en el caso de una  corutina esta se podrá poner en marcha por tres medios:

  • Cuando es llamada desde otra corutina usando yield: yield from corutina()
  • Por el método create_task() del bucle.
    • Nota: El método asyncio.async(corutina()) está considerado como obsoleto desde Python 3.4.4.

Promesas de futuro (Future)

Los objetos Future representan algo que está pendiente de ser acabado, por lo que se utilizan para poner en marcha una función cuando se ha completado. El método create_task() registra la llamada a una corutina en el bucle.

Ejemplo de Future:

import asyncio

@asyncio.coroutine
def llenar_plato(plato):
    print ("Llenando el plato...")
    yield from asyncio.sleep(5) # Devolvemos el control al bucle
    plato.set_result("arroz con pollo") # Establecemos el resultado

@asyncio.coroutine
def comer(plato):
    print ("Esperando a que llenen el plato")
    yield from plato
    print ("Plato lleno. ¡A comer {0}!.".format(plato.result()))

bucle = asyncio.get_event_loop()
plato = asyncio.Future()

bucle.create_task(llenar_plato(plato))

# En cuanto acaben de llenar el plato el programa finalizará
bucle.run_until_complete(comer(plato))

# Resultado:
# Llenando el plato...
# Esperando a que llenen el plato
# Plato lleno. ¡A comer arroz con pollo!.

Tareas (Task)

Las tareas son funciones derivadas de los objetos Future por lo que otras corutinas pueden suspender su ejecución esperando su finalización. Como cualquier otra función esta puede devolver un valor o levantar una excepción (teniendo en cuenta que se puede originar por su cancelación). Se puede entender una tarea como un envoltorio encargado de ejecutar una corutina.

De hecho cuando se ha utilizado run_until_complete anteriormente con una corutina como argumento internamente se ha creado una tarea. Lo mismo ha sucedido cuando se ha utilizando el método create_task, el cual devuelve un objeto Task.

A pesar de derivar de un Future se comporta de una forma un tanto diferente cuando son canceladas (esto  lo explicaré en otra entrada). Por último NO hay que crear un objeto directamente usando asyncio.Task(corutina()) .

Notas acerca de esta entrada

En esta entrada se han omitido diversos aspectos relacionados con el manejo de excepciones o los resultados no esperados de algunos métodos que pueden llevar a largas horas de depuración de código.

Fuentes de referencia:

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

w

Conectando a %s