Como sabemos, y si no ahora ya lo sabes, los threads en Python nos permiten mejorar el performance de nuestras aplicaciones de tal forma que podamos realizar múltiples tareas al mismo tiempo, al mismo tiempo entre comilla, ya que en la practica Python no soporta el multithreading per se, pero de ello ya hablamos anteriormente, si te interesa conocer más acerca del tema te comparto un vídeo el cual sin duda te será de mucha utilidad. Pero no nos desviemos, regresemos al tema de Threads.
Los threads son sin duda uno de nuestros mejores aliados en cuanto a programación concurrente se refiere, sin embargo como alguna vez lo mencionó el tío de Peter Parker: Un gran poder conlleva una gran responsabilidad y con los threads no es la excepción.
Uno pudiera pensar que si al utilizar dos threads es posible reducir el tiempo de ejecución hasta en un 50% entonces, entre más threads tengamos mejor ¿no? pues déjame decirte que no. instanciar (Crear) múltiples threads es una tarea costosa en cuanto a términos computacionales se refiere. veamos un ejemplo.
Imaginemos que tenemos la necesidad ejecutar 1,000 (mil) tareas de forma concurrente. En primera instancia uno pudiera pensar que lo más sencillo será crear y ejecutar 1,000 threads, uno por cada tarea y listo, problema resuelto. Esto en la teoría suena bien, pero en la práctica es una pésima idea. Sí hacemos esto no solo estaremos perjudicando el performance de nuestra aplicación, obteniendo un programa que incluso pueda llegar a ser más lento que si trabajamos con un solo thread. Y ya no mencionemos lo complicado que será testear la aplicación y mantener el código. Así que si tienes algún problema similar detente, no crees threads de forma indiscriminada, mejor apoyate de una pool de threads.
No me mal entiendan, los threads son un excelente opción para la trabajo de forma concurrente, claro, siempre y cuando hablemos de una cantidad pequeña de ellos. En nuestro ejemplo, crear 1,000 threads no es la mejor propuesta. En estos casos, en los casos necesitemos realizar un gran cantidad de tareas de forma concurrente, lo mejor que podemos hacer es apoyarnos de un pool de threads, y de esta forma reciclaremos threads ya existentes.
Pool de Threads
Comencemos con la pregunta obligada, ¿Qué es un pool de threads? verás, un pool, o una piscina, por su traducción al español, podemos definirlo como un mecanismo el cual nos permite crear una n cantidad threads los cuales vivirán el tiempo que nosotros deseemos, estos nos da la posibilidad de reutilizar dichos threads sin tener que crear nuevos. En otras palabras, un pool de threads nos permitirá reciclar threads ya existentes de tal forma que podamos asignarles nuevas tareas.
Esto puede sonar algo complejo, y de hecho lo es, así que si no te quedó del todo claro veamos un ejemplo.
import time
import logging
from concurrent.futures import ThreadPoolExecutor
logging.basicConfig(level=logging.DEBUG, format='%(threadName)s: %(message)s')
def super_task(a, b):
time.sleep(1) # Simulamos una tarea compleja.
logging.info('Terminamos la tarea compleja!!\n')
if __name__ == '__main__':
print('Creamos un pool con 2 threads')
executor = ThreadPoolExecutor(max_workers=2)
# Programamos y ejecutamos 4 tareas de forma concurrente
# Al solo existir 2 threads estas 2 tareas se ejecutarán primero
executor.submit(super_task, 10, 20)
executor.submit(super_task, 30, 40)
# Al solo existir 2 threads estas 2 tareas se ejecutarán después
executor.submit(super_task, 100, 200)
executor.submit(super_task, 300 400)
Para nosotros implementar un pool de threads en Python será necesario apoyarnos del módulo concurrent, utilizando la clase ThreadPoolExecutor.
Al utilizar el parámetro max_workers podremos definir la cantida de threads a crear. En mi ejemplo solo 2.
Una vez el pool haya sido creado podremos programar y ejecutar los threads a través del método submit. Este método funciona de la siguiente manera:
- El método Submit recibe como argumento la tarea a ejecutar de forma concurrente.
- Si existe un thread disponible, es decir un thread el cual no esté ejecutando nada de forma concurrente, se le es asigando la tarea.
- Una vez el thread tenga asignado una tarea el método submit se encarga de ejecutarlo.
- Se repite el proce.
En mi ejemplo, al programar 4 tareas para su ejecución y solo poseer 2 threads, el programa ejecutará primero las tareas con argumentos 10-20 y 30-40. Una vez un thread finalice, una nueva tarea se le será asignada.
Otra forma de crear un pool es mediante with.
with ThreadPoolExecutor(max_workers=2) as executor:
executor.submit(super_taks, 10, 20)
executor.submit(super_taks, 10, 20)
executor.submit(super_taks, 10, 20)
executor.submit(super_taks, 10, 20)
Algo muy importante que debemos tener en cuenta es que el método submit retornará un futuro. Si alguna vez programaste en JavasScript, podemos ver a los futuros de Ptyhon como las promesas, un valor que justo ahora nos esta disponible, pero en un futuro probablemente lo esté. Pero del tema de futuros ya estaremos hablando en otra ocasión, o si ya te interesa conocer más acerca del tema, te recuerdo que en CodigoFácilito tenemos un curso de programación concurrente en Python, en el cual se cubren este y otros temas muy interesantes, te invito a que le eches un vistazo, sin duda será de tu agrado.
Bien, esto sería todo por esta ocasión. Y recuerda, siempre que tengas la necesidad de realizar múltiples tareas de forma concurrente, tomate un par de segundos en pensar, realmente vale la pena crear decenas, cientos o miles de threads ¿Simplemente no es mejor crear un pool de threads? o mejor aún ¿Un pool de procesos?