Curso
Si acabas de empezar con Python y quieres seguir aprendiendo, haz el curso Introduction to Data Science in Python de DataCamp.

Introducción
El threading permite que distintas partes de tu proceso se ejecuten de forma concurrente (Fuente: RealPython). Estas partes suelen ser independientes y tienen una unidad de ejecución separada que pertenece al mismo proceso. Un proceso no es más que un programa en ejecución que contiene unidades que pueden ejecutarse en paralelo. Por ejemplo, un navegador web puede ser un proceso; una aplicación que usa varias cámaras a la vez puede ser otro proceso; un videojuego es otro ejemplo de proceso.
Dentro de un proceso aparece el concepto de múltiples hilos, o multithreading, donde varios hilos colaboran para alcanzar un objetivo común. La gran ventaja de usar hilos es que te permite ejecutar el programa en paralelo.
Veamos el concepto de threading con un ejemplo. Imagina una aplicación que cuenta los coches que entran y salen del aparcamiento de un centro comercial. El sistema tiene varias cámaras conectadas a un dispositivo central que monitorizan entradas y salidas. Cada cámara ejecuta un algoritmo para vigilar el flujo de coches y forma parte del mismo proceso o programa. Sin embargo, cada cámara, junto con el algoritmo que ejecuta, podría estar en un hilo independiente. Es más, la lectura de fotogramas de la cámara y la inferencia del algoritmo sobre esos fotogramas podrían ser dos hilos distintos.
Otro ejemplo sería un videojuego en el que el proceso necesita ejecutar tareas en paralelo como los gráficos, la interacción con el usuario y la red (en partidas multijugador) para ser siempre responsivo. Para lograrlo, recurre al multithreading, asignando a cada hilo la responsabilidad de una tarea independiente.
Un hilo tiene su propio flujo de ejecución, lo que significa que en el proceso suceden varias cosas a la vez.
Ten en cuenta que todo proceso tiene al menos un hilo, llamado main thread (hilo principal). Si tu programa no define ningún hilo adicional, al menos contará con el hilo principal, es decir, el propio proceso.
Si observas el diagrama superior, hay un proceso con el hilo principal que contiene variables globales y tres hilos diferentes t1, t2 y t3. Cada hilo tiene sus variables locales y su flujo de ejecución. Los hilos comparten en general los recursos del proceso; es decir, los recursos disponibles para el hilo principal también se comparten con los hilos t1, t2 y t3. Todos los hilos pueden acceder a las variables globales y, además, cuentan con sus variables locales.
Existen distintos tipos de hilos:
- Hilos de kernel
- Hilos de usuario
- Combinación de hilos de kernel y de usuario
Veamos ahora algunas ventajas de usar threading en tu programa:
-
El multithreading puede acelerar la ejecución si dispones de múltiples CPU.
-
También te permite realizar otras tareas mientras se ejecutan operaciones de E/S (I/O) gracias a varios hilos o incluso con el hilo principal más un hilo. Por ejemplo, la lectura de fotogramas de la cámara y la inferencia del algoritmo se gestionarán en hilos diferentes. Así, el algoritmo no tendrá que esperar a que llegue un fotograma y la lectura de fotogramas no tendrá que esperar a que termine la ejecución del algoritmo para leer el siguiente.
- Los hilos dentro de un mismo proceso pueden compartir la memoria y los recursos del hilo principal.
Retos del threading
-
Recuerda que Python, en su implementación CPython, limita la ejecución a un solo hilo a la vez; por tanto, threading no acelera todas las tareas. La razón principal es el Global Interpreter Lock (GIL).
Si quieres aprender más sobre el GIL, echa un vistazo a este tutorial.
-
Si buscas acelerar tareas intensivas de CPU, threading puede no ser la mejor solución. En esos casos, suele ser más útil el multiprocesamiento.
-
Para ejecutar un programa con muchos hilos, es necesario cambiar de uno a otro con rapidez; por eso, la planificación (scheduling) es clave en programas con muchos hilos.
-
El uso compartido de recursos también puede ser problemático, ya que todos los hilos comparten la misma memoria y las variables globales. Una operación en un hilo podría provocar un error de memoria en otro, o impedir que otro hilo obtenga memoria para realizar su tarea.
Threading en Python
-
En Python, el módulo de hilos es un módulo integrado llamado
threadingy puede importarse directamente. -
Como casi todo en Python se representa como objeto, los hilos también son objetos. Un hilo es capaz de:
- Almacenar datos,
- Guardarse en estructuras como diccionarios, listas, sets, etc.
- Pasarse como parámetro a una función.
-
Un hilo también puede ejecutarse como un proceso.
-
Un hilo en Python puede estar en varios estados, como:
- Espera (wait),
- Bloqueado (locked).
Veamos ahora cómo implementar threading en Python.
Módulo thread en Python 3
Ten en cuenta que Python 3 mantiene compatibilidad con el módulo thread de Python 2.7. En Python 3 puede importarse como módulo _thread. Veamos un ejemplo para entender el módulo _thread.
import _thread #thread module imported
import time #time module
Definamos una función llamada thread_delay que recibe dos parámetros: el nombre del hilo y el retardo. Dentro de esta función:
- Defines un contador que empieza en cero,
- Luego iteras con un bucle while que se ejecutará tres veces,
- Dentro del while llamas a
time.sleep()para añadir un retardo; esto te ayudará a ver cómo se ejecuta el hilo. El retardo se expresa en segundos, - Después incrementas el contador en 1,
- Para comprobar que el hilo se ejecuta bien, imprimes el nombre del hilo y la hora a la que se ejecuta.
def thread_delay(thread_name, delay):
count = 0
while count < 3:
time.sleep(delay)
count += 1
print(thread_name, '-------->', time.time())
Ahora, para ejecutar la función anterior en un hilo, usa el método start_new_thread, que pertenece al módulo _thread.
Veamos la docstring del método start_new_thread.
?_thread.start_new_thread

Pasa la función thread_delay sin paréntesis y dos argumentos: el nombre del hilo y el retardo (para poder visualizar la ejecución, ya que es muy rápida).
_thread.start_new_thread(thread_delay, ('t1', 1))
_thread.start_new_thread(thread_delay, ('t2', 3))
10916
t1 --------> 1588077856.4221454
t1 --------> 1588077857.4418247
t2 --------> 1588077858.422453
t1 --------> 1588077858.4431
t2 --------> 1588077861.4371448
t2 --------> 1588077864.438915
En la salida puedes ver que el hilo t1 empieza a ejecutarse primero. Mientras tanto, t2 espera por su retardo de 3 segundos y, en cuanto termina, se ejecuta. El retardo de t1 es de solo 1 segundo.
Cambiemos ahora el retardo de t2 a 5 segundos, de modo que t2 empiece cuando t1 ya haya terminado, ya que t1 tardará solo 3 segundos en completarse.
_thread.start_new_thread(thread_delay, ('t1', 1))
_thread.start_new_thread(thread_delay, ('t2', 5))
14632
t1 --------> 1588078001.4087512
t1 --------> 1588078002.4272866
t1 --------> 1588078003.432218
t2 --------> 1588078005.4082909
t2 --------> 1588078010.4268317
t2 --------> 1588078015.4283602
Implementar threading con el módulo threading
Use mos el mismo ejemplo de antes, pero esta vez con el módulo threading en lugar de _thread.
import threading
import time
def thread_delay(thread_name, delay):
count = 0
while count < 3:
time.sleep(delay)
count += 1
print(thread_name, '-------->', time.time())
Dentro del módulo threading está la clase Thread, similar en espíritu a la función start_new_thread del módulo _thread.
Consultemos la docstring de la clase Thread, que admite varios parámetros como group, target (una función), args, etc.
?threading.Thread

En el constructor de Thread, pasarás la función objetivo thread_delay y los argumentos de esa función.
t1 = threading.Thread(target=thread_delay, args=('t1', 1))
t2 = threading.Thread(target=thread_delay, args=('t2', 3))
En el módulo threading, para ejecutar el hilo utilizas el método start(), encargado de ponerlo en marcha.
?t1.start

t1.start()
t2.start()
t1 --------> 1588081121.7790291
t1 --------> 1588081122.7962046
t2 --------> 1588081123.7789369
t1 --------> 1588081123.7980876
t2 --------> 1588081126.7961557
t2 --------> 1588081129.7995002
También puedes usar el método join, que hace que el programa espere hasta que acaben todos los hilos. Cualquier código después de join se ejecutará solo cuando estos hilos hayan terminado.
?t1.join

t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
t1 --------> 1588081798.7118263
t1 --------> 1588081799.7231226
t2 --------> 1588081800.7129223
t1 --------> 1588081800.730732
t2 --------> 1588081803.7298765
t2 --------> 1588081806.7309563
Thread execution is complete!
Calculemos ahora el volumen de un cubo y de un cuadrado usando threading; es bastante directo.
def volume_cube(a):
print ("Volume of Cube:", a*a*a)
def volume_square(a):
print ("Volume of Square:", a*a)
t1 = threading.Thread(target=volume_cube, args=(2))
t2 = threading.Thread(target=volume_square, args=(3))
t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
Thread execution is complete!
Exception in thread Thread-9:
Traceback (most recent call last):
File "C:\Program Files\Anaconda3\lib\threading.py", line 926, in _bootstrap_inner
self.run()
File "C:\Program Files\Anaconda3\lib\threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
TypeError: volume_square() argument after * must be an iterable, not int
Exception in thread Thread-8:
Traceback (most recent call last):
File "C:\Program Files\Anaconda3\lib\threading.py", line 926, in _bootstrap_inner
self.run()
File "C:\Program Files\Anaconda3\lib\threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
TypeError: volume_cube() argument after * must be an iterable, not int
¿Cómo? En cuanto ejecutaste start apareció un error. Es porque el parámetro args espera una tupla, y como volume_cube y volume_square solo esperan un parámetro, necesitas poner una coma en args tras indicar el primer argumento de las funciones.
t1 = threading.Thread(target=volume_cube, args=(2,))
t2 = threading.Thread(target=volume_square, args=(3,))
t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
Volume of Cube: 8
Volume of Square: 9
Thread execution is complete!
Veamos ahora cómo usar threading creando una subclase.
Threading como subclase
En esta sección aprenderás a crear una subclase de la clase Thread, disponible en el módulo threading, y luego crear instancias de hilo t1 y t2 a partir de esa clase.
Usaremos de nuevo la función thread_delay.
class DataCampThread(threading.Thread):
def __init__(self, name, delay):
threading.Thread.__init__(self)
self.name = name
self.delay = delay
def run(self):
print('Starting Thread:', self.name)
thread_delay(self.name,self.delay)
print('Execution of Thread:', self.name, 'is complete!')
t1 = DataCampThread('t1', 1)
t2 = DataCampThread('t2', 3)
t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
Starting Thread: t1
Starting Thread: t2
t1 --------> 1588089083.4879806
t1 --------> 1588089084.489222
t1 --------> 1588089085.4909666
Execution of Thread: t1 is complete!
t2 --------> 1588089085.4979634
t2 --------> 1588089088.499365
t2 --------> 1588089091.5012596
Execution of Thread: t2 is complete!
Thread execution is complete!
Entendamos el código paso a paso:
-
Definiste una clase
DataCampThready, como argumento de clase, pasastethreading.Thread. Lo haces para crear una subclase del módulo threading. Así puedes usar todos los métodos disponibles enthreading.Thread, de forma similar a la herencia. -
Después definiste el método init de
DataCampThready pasaste el nombre y el retardo. Este init actúa como constructor, de forma análoga a los parámetros que pasaste athreading.Thread, pero aquí para la claseDataCampThread. -
Luego inicializas el init de la clase base
threading.Thready asignas name y delay. -
Por último, defines el método
run. Recuerda que aquí usaste el métodostart, que bajo el capó llama arun. Como has heredado de la clase principal, puedes modificarrun. Así, cuando llames astart, se reflejarán los cambios que hagas enrun.
Conclusión
¡Enhorabuena por completar el tutorial!
Este tutorial ha sido una introducción básica al threading en Python. Sin embargo, es un tema amplio y esencial, y puede que hayan quedado conceptos por explorar. No dudes en investigar otros métodos como active count, current thread, etc.
Si quieres profundizar, consulta la documentación de Python sobre paralelismo basado en hilos.
Si estás empezando con Python y quieres aprender más, haz el curso Introduction to Data Science in Python de DataCamp.
