En el anterior artículo de la serie nos introdujimos más a fondo en el manejo de hilos en Python. En esta nueva entrega de multiprocesamiento en Python vamos a ver los threads aún más a fondo aprendiendo a enumerar todos los threads, heredar del objeto Thread y usar threads con temporizador.
En cada nuevo artículo de la serie iremos profundizando cada vez más en los entresijos del multiprocesamiento en Python y pondremos nuestros conocimientos en práctica escribiendo una serie de benchmarks y tests para comprobar que nuestras aplicaciones se comportan como esperamos de ellas.
Enumerar los hilos
Como vimos en el artículo anterior, es posible crear hilos como daemons y debemos utilizar el métodojoin() de forma explícita para esperar a que uno de ellos finalice.
Realmente no es necesario mantener un manejador explícito a cada hilo daemonizado para asegurarnos de que hayan completado su tarea antes de salir del proceso. Podemos utilizar el método enumerate() que devuelve una lista de todos las instancias de hilos activos.
Hay que tener cuidado porque la lista incluye el hilo principal (main thread) y hacer un join() al hilo principal produce de forma irremisible un deadlock (¿recuerdas lo que es un deadlock de la serie de multiprocesamiento en C++?. En caso contrario revisa este post).
Para nuestros ejemplos de hoy vamos a utilizar la misma configuración para el módulo logging que usamos en el artículo anterior así que voy a obviarla del código de los ejemplos, si necesitas revisarla comprueba el anterior post de la serie:
import random
import threading
import time
import logging
def worker():
ct = threading.currentThread()
p = random.randint(1, 5)
logging.debug('durmiendo %s', p)
time.sleep(p)
logging.debug('despertando y saliendo')
return
for i in range(4):
tr = threading.Thread(target=worker)
tr.setDaemon(True)
tr.start()
# hilo principal
mt = threading.currentThread()
for th in threading.enumerate():
# si es el hilo principal saltar o entraremos en deadlock
if th is mt:
continue
logging.debug('haciendo join a %s', th.getName())
th.join()La salida del código anterior puede variar entre ejecuciones debido a que el tiempo de espera es aleatorio:
python enumera_hilos.py
[DEBUG] – Thread-1 : durmiendo 5
[DEBUG] – Thread-2 : durmiendo 2
[DEBUG] – Thread-3 : durmiendo 1
[DEBUG] – Thread-4 : durmiendo 5
[DEBUG] – MainThread : haciendo join a Thread-1
[DEBUG] – Thread-3 : despertando y saliendo
[DEBUG] – Thread-2 : despertando y saliendo
[DEBUG] – Thread-1 : despertando y saliendo
[DEBUG] – MainThread : haciendo join a Thread-4
[DEBUG] – Thread-4 : despertando y saliendo
[DEBUG] – MainThread : haciendo join a Thread-3
[DEBUG] – MainThread : haciendo join a Thread-2
Derivando la clase Thread
Todo hilo debe realizar algunas operaciones básicas de inicialización y entonces llamar a su método runrun() que llama a la función objetivo pasada en el constructor. Para crear una clase derivada de Thread es necesario sobreescribir el método run():
import threading
import logging
class GenThread(threading.Thread):
def run(self):
logging.debug('en ejecución')
return
for i in range(10):
gth = GenThread()
gth.start()La salida del código anterior sería:
python derivada.py
[DEBUG] – Thread-1 : en ejecución
[DEBUG] – Thread-2 : en ejecución
[DEBUG] – Thread-3 : en ejecución
[DEBUG] – Thread-4 : en ejecución
[DEBUG] – Thread-5 : en ejecución
[DEBUG] – Thread-6 : en ejecución
[DEBUG] – Thread-7 : en ejecución
[DEBUG] – Thread-8 : en ejecución
[DEBUG] – Thread-9 : en ejecución
[DEBUG] – Thread-10 : en ejecuciónLa implementación del objeto Thread y su constructor no permite acceder a los parámetros pasados al constructor de forma sencilla desde una subclase. Si queremos pasar argumentos a nuestra clase, debemos redefinir el constructor y guardar los valores en una instancia visible para la subclase:
import threading
import logging
class GenThread(threading.Thread):
def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, verbose=None):
threading.Thread.__init__(self, group=group, target=target, name=name, verbose=verbose)
self.args = args
self.kwargs = kwargs
return
def run(self):
logging.debug('en ejecución con parámetros %s y %s', self.args, self.kwargs)
return
for i in range(10):
gth = GenThread(args=(i,), kwargs={'g' : 'G', 'e' : 'E'})
gth.start()La salida del código anterior no es mucho más especial que la anterior:
[DEBUG] – Thread-1 : en ejecución con parámetros (0,) y {'e': 'E', 'g': 'G'}
[DEBUG] – Thread-2 : en ejecución con parámetros (1,) y {'e': 'E', 'g': 'G'}
[DEBUG] – Thread-3 : en ejecución con parámetros (2,) y {'e': 'E', 'g': 'G'}
[DEBUG] – Thread-4 : en ejecución con parámetros (3,) y {'e': 'E', 'g': 'G'}
[DEBUG] – Thread-5 : en ejecución con parámetros (4,) y {'e': 'E', 'g': 'G'}
[DEBUG] – Thread-6 : en ejecución con parámetros (5,) y {'e': 'E', 'g': 'G'}
[DEBUG] – Thread-7 : en ejecución con parámetros (6,) y {'e': 'E', 'g': 'G'}
[DEBUG] – Thread-8 : en ejecución con parámetros (7,) y {'e': 'E', 'g': 'G'}
[DEBUG] – Thread-9 : en ejecución con parámetros (8,) y {'e': 'E', 'g': 'G'}
[DEBUG] – Thread-10 : en ejecución con parámetros (9,) y {'e': 'E', 'g': 'G'}Esta subclase es muy sencilla y comparte la API con su clase base Thread pero se puede crear una clase todo lo compleja que queramos para que cumpla con nuestra necesidades.
Threads con temporizador
Una de las razones para derivar una clase del objetoThread queda perfectamente retratada en la clase Timer que también está incluido en el módulo threading y que nos provee de una manera de lanzar nuestros hilos tras un retraso que además puede ser cancelado en cualquier punto de ese retraso:
import threading
import time
import logging
def retraso():
logging.debug('worker en ejecución')
return
th1 = threading.Timer(2, restraso)
th1.setName('th1')
th2 = threading.Timer(2, restraso)
th2.setName('th2')
logging.debug('lanzando temporizadores')
th1.start()
th2.start()
logging.debug('esperando antes de cancelar a %s', th2.getName())
time.sleep(1)
logging.debug('cancelando a %s', th2.getName())
th2.cancel()
logging.debug('hecho')En el ejemplo anterior el segundo Timer no debería ejecutarse nunca y el primero se ejecuta de forma aparente después del resto del programa. Al no ser un hilo daemon hace join de forma implícita cuando el MainThread termina.
python timer.py
[DEBUG] – MainThread : lanzando temporizadores
[DEBUG] – MainThread : esperando antes de cancelar a th2
[DEBUG] – MainThread : cancelando a th2
[DEBUG] – MainThread : hecho
[DEBUG] – th1 : worker en ejecución
En el próximo artículo de la serie seguiremos indagando cada vez más profúndamente en el multiprocesamiento tocando temas como la comunicación entre hilos y el acceso a los recursos.
En Genbeta Dev | Multiprocesamiento en Python