Python ejecutar funciones de forma asíncrona y en background

3 minutos

Python

Disponemos de diversas bibliotecas y sintaxis nativa en Python para ejecutar instrucciones de fondo, permitiendo continuar las acciones mientras por detrás se completan. De este modo evitaremos bloqueos del hilo principal o esperas innecesarias mientras esperamos una respuesta (un API, disco duro, calculo complejo…). En mi caso es muy útil para enviar correos electrónicos, ya que no es necesario esperar a que se envíen todos los correos para continuar con el resto del código. O también cuando dependo de una API externa que tarda en responder. Estoy seguro que tú también tienes casos similares.

Y entre todas las posibilidades que puedes encontrar, te voy a dejar un par de ejemplos de como lograrlo usando threading o futures.

Threading

Es la manera más sencilla.

En el siguiente ejemplo lanzamos function_1 en background.

import threading

# Función que se ejecutará de forma asíncrona

def funcion_1(parametro_1):
    pass

# Declaro un hilo de ejecución. "args" debe ser una Tupla.

threading_emails = threading.Thread(target=funcion_1, args=(parametro_1,))

# Lo lanzo

threading_emails.start()

# Resto de mi código que se ejecutará de forma paralela
...


Veamos un ejemplo con código real.

import threading
import time

# Función que se ejecutará de forma asíncrona

def saluda(nombre):
    # Espera 2 segundos
    time.sleep(2)
    print(f"Hola {nombre}")

# Declaro un hilo de ejecución

threading_emails = threading.Thread(target=saluda, args=("Snake",))

# Lo lanzo

threading_emails.start()

# Resto de mi código que se ejecutará de forma paralela

print("Soy el resto del código")

Se imprimirá:

Soy el resto del código
Hola Snake

En el caso de necesitar de ejecutar muchas funciones asíncronas, podemos agruparlas en una sola.

import threading

# Funciones que quiero ejecutar de forma asíncrona

def funcion_1(parametro_1):
    pass

def funcion_2():
    pass

def funcion_3(parametro_2):
    pass

def funcion_4(parametro_1, parametro_2):
    pass

# Función que engloba a todas

def conjunto_de_funciones(parametro_1, parametro_2):
    funcion_1(parametro_1)
    funcion_2()
    funcion_3(parametro_2)
    funcion_4(parametro_1, parametro_2)

# Declaro un hilo de ejecución

threading_emails = threading.Thread(target=conjunto_de_funciones, args=(parametro_1, parametro_2))

# Lo lanzo

threading_emails.start()

# Resto de mi código que se ejecutará de forma paralela
...

Lamentablemente posee una pega: No podemos capturar el return, o el valor, de la función. Para solucionarlo podemos utilizar una alternativa llamada futures.

Futures

La biblioteca concurrent.futures es un poco más compleja pero tiene algunas ventajas respecto a threading. Permite capturar el resultado de las funciones e identificar el hilo que la ejecuta.

La estructura sería tal que así:

import concurrent.futures

# Función que se ejecutará de forma asíncrona
def funcion_a_ejecutar(argumento_1, argumento_2):
    pass

# Declaro un hilo de ejecución. Con `max_workers` le indico el número de hilos
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
    # Envío la función a ejecutar y sus argumentos
    future = executor.submit(funcion_a_ejecutar, argumento_1, argumento_2)
    # Aquí puedes hacer otras cosas mientras la función se ejecuta
    # ...
    # Obtener el resultado
    resultado = future.result()

En el caso que desees ejecutar varias funciones en paralelo, puedes hacerlo de la siguiente manera:

import concurrent.futures

# Función que se ejecutará de forma asíncrona
def funcion_a_ejecutar(argumento_1, argumento_2):
    pass

# Declaro un hilo de ejecución. Con `max_workers` le indico el número de hilos
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
    # Envío la función a ejecutar y sus argumentos
    mis_funciones = {
        executor.submit(funcion_a_ejecutar, argumento_1, argumento_2):
        "Etiqueta de la función 1",
        executor.submit(funcion_a_ejecutar, argumento_3, argumento_4):
        "Etiqueta de la función 2",
    }
    # Aquí puedes hacer otras cosas mientras las funciones se ejecutan
    # ...
    # Obtener los resultados
    for future in concurrent.futures.as_completed(mis_funciones):
        etiqueta = mis_funciones[future]
        try:
            if etiqueta == "Etiqueta de la función 1":
                print("Resultado de la función 1: " + str(future.result()))
            elif etiqueta == "Etiqueta de la función 2":
                print("Resultado de la función 2: " + str(future.result()))
        except Exception as exc:
            print('%r generated an exception: %s' % (future, exc))

Las etiquetas deben ser únicas. Hay mil trucos para hacer que sean únicas, como añadir un id si estas usando datos de una base de datos, un uuid, una secuencia de números, enumerate, etc.

Vamos a ver un ejemplo sencillo donde sumo 4 veces 2 números y los imprimo en pantalla. En lugar de hacerlo de forma secuencial (uno detrás de otro), lo haré de forma asíncrona (en paralelo).

import time
from uuid import uuid
import concurrent.futures


def mi_funcion_de_tiempo(num1, num2):
    return num1 + num2

with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
    ids = ['Suma 1', 'Suma 2', 'Suma 3', 'Suma 4']
    futuros = [
        executor.submit(mi_funcion_de_tiempo, 2, 4),
        executor.submit(mi_funcion_de_tiempo, 3, 5),
        executor.submit(mi_funcion_de_tiempo, 1, 1),
        executor.submit(mi_funcion_de_tiempo, 10, 20)
    ]
    for future in concurrent.futures.as_completed(futuros):
        try:
            resultado = future.result()
            print(f"Resultado: {resultado}")
        except Exception as exc:
            print('%r generated an exception: %s' % (future, exc))

Espero que te sea de ayuda.

Si tienes alguna duda, no dudes en dejarla en los comentarios.

Esta obra está bajo una Licencia Creative Commons Atribución-NoComercial-SinDerivadas 4.0 Internacional.

Atribución/Reconocimiento-NoComercial-SinDerivados 4.0 Internacional

¿Me invitas a un café? ☕

Puedes hacerlo usando el terminal.

ssh customer@andros.dev -p 5555

Comentarios

{{ comments.length }} comentarios

Nuevo comentario

Nueva replica  {{ formatEllipsisAuthor(replyComment.author) }}

Acepto la política de Protección de Datos.

Escribe el primer comentario

Tal vez también te interese...