El módulo `concurrent` en Python

El módulo `concurrent` en Python

En este artículo, explicaremos el módulo concurrent en Python.

Mientras aclaramos los conceptos de concurrencia y paralelismo, explicaremos cómo implementar el procesamiento asíncrono utilizando el módulo concurrent con ejemplos prácticos.

YouTube Video

El módulo concurrent en Python

Al acelerar el procesamiento en Python, es importante tener en cuenta las diferencias entre la concurrencia y el paralelismo. El módulo concurrent es un recurso importante para manejar de forma segura y sencilla el procesamiento asíncrono teniendo en cuenta estas diferencias.

La diferencia entre concurrencia y paralelismo

  • Concurrencia significa diseñar un proceso de manera que múltiples tareas avanzan alternando entre ellas en pequeñas unidades de trabajo. Incluso si las tareas no se ejecutan realmente al mismo tiempo, aprovechar los "tiempos de espera" permite hacer el proceso completo más eficiente.

  • Paralelismo es un mecanismo que ejecuta físicamente múltiples tareas de forma simultánea. Al utilizar múltiples núcleos de CPU, el procesamiento avanza en paralelo.

Ambas son técnicas para acelerar el procesamiento, pero la concurrencia es una cuestión de diseño sobre 'cómo proceder', mientras que el paralelismo es una cuestión de ejecución sobre 'cómo se ejecuta', lo que las hace fundamentalmente diferentes.

¿Qué es el módulo concurrent?

concurrent es una biblioteca estándar en Python que proporciona una API de alto nivel para manejar la concurrencia y el paralelismo de manera segura y sencilla. Está diseñada para que puedas enfocarte en 'ejecutar tareas' sin preocuparte por operaciones de bajo nivel como crear y gestionar hilos o procesos.

Funciones de ThreadPoolExecutor y ProcessPoolExecutor

El módulo concurrent ofrece dos opciones principales dependiendo de la naturaleza de la tarea.

  • ThreadPoolExecutor Esto es adecuado para implementaciones concurrentes, especialmente tareas con muchas esperas de E/S, como operaciones de red o archivos. Al alternar entre tareas, se hace un uso efectivo del tiempo de espera.

  • ProcessPoolExecutor Esta implementación está orientada al procesamiento paralelo y es adecuada para tareas intensivas en CPU. Utiliza múltiples procesos para aprovechar al máximo los núcleos de CPU disponibles en paralelo.

Así, una característica principal del módulo concurrent es que proporciona una estructura que permite elegir adecuadamente entre concurrencia y paralelismo según sea necesario.

Fundamentos de ThreadPoolExecutor (para tareas de E/S)

ThreadPoolExecutor es adecuado para tareas dependientes de E/S, como la comunicación de red y las operaciones de archivos. Distribuye las tareas entre múltiples hilos, aprovechando eficientemente el tiempo de espera.

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(fetch_data, i) for i in range(5)]
11
12    for future in futures:
13        print(future.result())
  • En este ejemplo, múltiples tareas de E/S que esperan un segundo se ejecutan de forma concurrente. Al utilizar submit, las llamadas a funciones se registran como tareas asíncronas, y al llamar a result(), puedes esperar la finalización y obtener los resultados; esto te permite implementar procesamiento concurrente que aprovecha el tiempo de espera de forma concisa.

Concurrencia simple usando map

Si no es necesario un control complejo, usar map puede hacer tu código más conciso.

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    results = executor.map(fetch_data, range(5))
11
12    for result in results:
13        print(result)
  • En este ejemplo, múltiples tareas de E/S se ejecutan de forma concurrente usando ThreadPoolExecutor.map. Como map devuelve los resultados en el mismo orden que las entradas, puedes escribir código similar al procesamiento secuencial, y la ejecución concurrente es posible sin preocuparse por el procesamiento asíncrono—esta es una gran ventaja.

Fundamentos de ProcessPoolExecutor (para tareas intensivas en CPU)

Para cómputos pesados que utilizan completamente el CPU, debes usar procesos en lugar de hilos. Esto permite evitar la limitación del Global Interpreter Lock (GIL).

 1from concurrent.futures import ProcessPoolExecutor
 2
 3def heavy_calculation(n):
 4    # Simulate a CPU-bound task
 5    total = 0
 6    for i in range(10_000_000):
 7        total += i * n
 8    return total
 9
10if __name__ == "__main__":
11    with ProcessPoolExecutor(max_workers=4) as executor:
12        results = executor.map(heavy_calculation, range(4))
13
14        for result in results:
15            print(result)

En este ejemplo, los cálculos que requieren mucha CPU se ejecutan en paralelo usando ProcessPoolExecutor. Como implica la creación de procesos, se requiere una protección __main__, lo que permite el procesamiento paralelo seguro utilizando múltiples núcleos de CPU.

Procesamiento por orden de finalización usando as_completed

as_completed es útil cuando deseas manejar los resultados en el orden en que se completan.

 1from concurrent.futures import ThreadPoolExecutor, as_completed
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(fetch_data, i) for i in range(5)]
11
12    for future in as_completed(futures):
13        print(future.result())
  • En este ejemplo, múltiples tareas asíncronas se ejecutan simultáneamente y los resultados se recuperan en el orden en que finalizan. Usar as_completed te permite manejar rápidamente los resultados sin importar el orden de las tareas, lo cual es adecuado para mostrar el progreso o situaciones que requieren manejo secuencial.

Manejo de excepciones

En concurrent, las excepciones se lanzan cuando llamas a result().

 1from concurrent.futures import ThreadPoolExecutor
 2
 3def risky_task(n):
 4    # Simulate a task that may fail for a specific input
 5    if n == 3:
 6        raise ValueError("Something went wrong")
 7    return n * 2
 8
 9with ThreadPoolExecutor() as executor:
10    futures = [executor.submit(risky_task, i) for i in range(5)]
11
12    for future in futures:
13        try:
14            print(future.result())
15        except Exception as e:
16            print("Error:", e)
  • Este ejemplo demuestra que, incluso si algunas tareas lanzan excepciones, las demás continúan su ejecución y puedes manejar las excepciones individualmente al recuperar los resultados. Al usar Future de concurrent, es importante que los éxitos y fallos en el procesamiento asíncrono se puedan manejar de forma segura.

Guía para elegir entre hilos y procesos

Para utilizar la concurrencia y el paralelismo eficazmente, es importante escoger el enfoque correcto según la naturaleza de la tarea.

En la práctica, los siguientes criterios pueden ayudarte a decidir.

  • Para procesos con muchas esperas de E/S, como comunicaciones u operaciones de archivos, utilice ThreadPoolExecutor.
  • Para tareas pesadas e intensivas en CPU, utiliza ProcessPoolExecutor.
  • Si hay muchas tareas simples, utilizar map te permite escribir un código más conciso.
  • Si es importante el control preciso del orden de ejecución o el manejo de excepciones, combina submit con as_completed.

Ventajas de usar concurrent

Al usar el módulo concurrent, puedes manejar el procesamiento asíncrono de forma segura e intuitiva.

Los principales beneficios son los siguientes:.

  • No tienes que preocuparte por la gestión de hilos o procesos a bajo nivel.
  • Está incluido en la biblioteca estándar de Python, así que puedes usarlo con confianza.
  • El código se vuelve más legible y mantenible.
  • Es ideal como primer paso para aprender sobre concurrencia y paralelismo.

Seguir estas pautas puede reducir significativamente los errores en implementaciones que usan concurrent.

Resumen

El módulo concurrent es la opción estándar para la concurrencia y el paralelismo prácticos en Python. Permite aumentar el rendimiento sin cambiar demasiado el contenido de tu procesamiento, lo cual es un beneficio importante en la práctica real. Al usar concurrent, puedes implementar procesamiento asíncrono de forma concisa mientras gestionas de manera segura el manejo de excepciones y el control de ejecución.

Puedes seguir el artículo anterior utilizando Visual Studio Code en nuestro canal de YouTube. Por favor, también revisa nuestro canal de YouTube.

YouTube Video