¿QUÉ SON LOS THREADS?
Para que una computadora sea útil
es necesario que podamos hacer más de una cosa a la vez. La cuestión es cómo
lograr esto en computadoras con una sola CPU, la solución fue que en realidad
no ejecutar varios procesos a la vez, sino que los procesos se van turnando y,
dada la velocidad a la que ejecutan las instrucciones, nosotros tenemos la
impresión de que las tareas se ejecutan de forma paralela como si tuviéramos
multitarea real.
Cada vez que un proceso distinto
pasa a ejecutarse es necesario realizar lo que se llama un cambio de contexto,
durante el cual se salva el estado del programa que se estaba ejecutando a
memoria y se carga el estado del programa que va a entrar a ejecutarse, sin
embargo esto puede ser relativamente lento, por lo que a veces es mucho más
eficaz utilizar lo que se conoce como threads, hilos de ejecución, o procesos
ligeros.
Los threads son un concepto
similar a los procesos: también se trata de código en ejecución. Sin embargo
los threads se ejecutan dentro de un proceso, y los threads del proceso
comparten recursos entre sí. El sistema operativo necesita menos recursos para
crear y gestionar los threads, y el cambio de contexto es más rápido. Además,
dado que los threads comparten el mismo espacio de memoria global, es sencillo
compartir información entre ellos: cualquier variable global que tengamos en
nuestro programa es vista por todos los threads.
EL GIL
La ejecución de los threads en
Python está controlada por el GIL (Global Interpreter Lock) de forma que sólo
un thread puede ejecutarse a la vez, independientemente del número de
procesadores con el que cuente la máquina.
Por defecto el cambio de thread
se realiza cada 10 instrucciones de bytecode, aunque se puede modificar
mediante la función sys.setcheckinterval. También se cambia de thread cuando el
hilo se pone a dormir con time.sleep o cuando comienza una operación de
entrada/salida, las cuales pueden tardar mucho en finalizar.
Para minimizar un poco el efecto
del GIL en el rendimiento de nuestra aplicación es conveniente llamar al
intérprete con el flag -O, lo que hará que se genere un bytecode optimizado con
menos instrucciones, y, por lo tanto, menos cambios de contexto.
THREADS EN PYTHON
El trabajo con threads se lleva a
cabo en Python mediante el módulo thread. Este módulo es opcional y dependiente
de la plataforma, también contamos con el módulo threading que se apoya en el
primero para proporcionarnos una API orientada a objetos.
El módulo threading contiene una
clase Thread que debemos extender para crear nuestros propios hilos de
ejecución. El método run contendrá el código que queremos que ejecute el
thread. Si queremos especificar nuestro propio constructor, este deberá llamar
a threading.Thread.__init__(self) para inicializar el objeto correctamente.
Para que el thread comience a
ejecutar su código basta con crear una instancia de la clase que acabamos de
definir y llamar a su método start. El código del hilo principal y el del que
acabamos de crear se ejecutarán de forma concurrente.
El método join se utiliza para
que el hilo que ejecuta la llamada se bloquee hasta que finalice el thread
sobre el que se llama. El método join puede tomar como parámetro un número en
coma flotante indicando el número máximo de segundos a esperar. Podemos observar estos elementos
en el siguiente ejemplo:
import threading class MiThread(threading.Thread): def __init__(self, num): threading.Thread.__init__(self) self.num = num def run(self): print ("Soy el hilo", self.num) print ("Soy el hilo principal") for i in range(0, 10): t = MiThread(i) t.start() t.join()
SINCRONIZACIÓN
Uno de los mayores problemas a
los que tenemos que enfrentarnos al utilizar threads es la necesidad de
sincronizar el acceso a ciertos recursos por parte de los threads. Entre los
mecanismos de sincronización que tenemos disponibles en el módulo threading se
encuentran los siguientes:
Candado
Los locks, también llamados mutex
son objetos con dos estados posibles: adquirido o libre. Cuando un thread
adquiere el candado, los demás threads que pidan adquirirlo se bloquearán hasta
que el thread que lo ha adquirido libere el candado, momento en el cual podrá
entrar otro thread.
El candado se representa mediante
la clase Lock. Llamando a Lock.acquire() el hilo bloqueará el Lock, de forma
que el siguiente hilo que llame a Lock.acquire() se quedará a la espera de que
el Lock se desbloquee. La llamada a Lock.release() desbloquea el Lock, haciendo
que el hilo que estaba en espera continúe. Un ejemplo se muestra a continuación:
from threading import Thread, Lock import time class MiHilo(Thread): def __init__ (self, inicio, fin, bloqueo): Thread.__init__(self) self.inicio = inicio self.fin = fin self.bloqueo = bloqueo def run (self): bloqueo.acquire() for i in range(self.inicio,self.fin): print ("contador = "+str(i)) time.sleep(0.2) bloqueo.release() if __name__ == '__main__': bloqueo = Lock() bloqueo.acquire() hilo = MiHilo(0,10, bloqueo) hilo.start() time.sleep(1) for i in range (10,20): print ("main = "+str(i)) time.sleep(0.1) bloqueo.release()
Esta clase Lock es muy simple.
Cualquier hilo, puede liberar a otro sin importar si lo haya bloquedado o no, además
si un hilo llama él mismo dos veces a acquire(), se queda bloqueado en la
segunda llamada. Una mejor opción es RLock. En este candado solo el que haya
llamado al método acquire() puede liberarlo con el método release() y un mismo
hilo puede llamar varias veces a acquire() sin quedarse bloqueado, pero tiene
que hacer el mismo número de llamadas a release() para desbloquearlo. El
ejemplo siguiente muestra su funcionamiento:
from threading import Thread, RLock import time class MiHilo(Thread): def __init__ (self, inicio, fin, bloqueo): Thread.__init__(self) self.inicio = inicio self.fin = fin self.bloqueo = bloqueo def run (self): bloqueo.acquire() for i in range(self.inicio,self.fin): print ("contador = "+str(i)) time.sleep(0.2) bloqueo.release() if __name__ == '__main__': bloqueo = RLock() bloqueo.acquire() hilo = MiHilo(0,10, bloqueo) hilo.start() time.sleep(1) for i in range (10,20): print ("main = "+str(i)) bloqueo.acquire() time.sleep(0.1) bloqueo.release() print ("El hilo todavia no ha comenzado") for i in range(10,20): bloqueo.release()
Semáforos
Un semáforo permite acceder a un determinado recurso a un
número máximo de hilos simultáneamente. Si hay más hilos que el máximo
permitido, los pone en espera y los va dejando pasar según van terminando los
que están activos. Un semáforo actúa como un contador con un valor inicial.
Cada vez que un hilo llama a Semaphore.acquire(),
el contador se decrementa en 1 y se deja pasar al hilo. En el momento que el
contador se hace cero, NO se deja pasar al siguiente hilo que llame a acquire(), sino que lo deja
bloqueado. Cada vez que se llama a Semaphore.release(),
el contador se incrementa en 1. Si se hace igual a cero, libera al siguiente
hilo en la cola de espera. El ejemplo de su funcionamiento se muestra a continuación:
from threading import Thread, Semaphore import time, random class MiHilo(Thread): def __init__(self, numero_hilo, semaforo): Thread.__init__(self) self.semaforo=semaforo self.numero_hilo = numero_hilo def run(self): semaforo.acquire() print ("Entra hilo "+str(self.numero_hilo)) time.sleep(random.randrange(1,10,1)) print ("Fin hilo " + str(self.numero_hilo)) semaforo.release() if __name__ == '__main__': random.seed() semaforo = Semaphore(5) for i in range(0,10): hilo=MiHilo(i,semaforo) hilo.start() print ("Arrancado hilo "+str(i))
Condiciones
Algo muy común es tener un hilo esperando por unos datos para
tratarlos. Otro hilo es el encargado de proporcionar esos datos y avisar al
primer hilo de que ya están disponibles. Para facilitar este tipo de uso
tenemos la clase threading.Condition.
En primer lugar, creamos la Condition.
El hilo que debe esperar por los datos, debe llamar al método Condition.acquire() y luego al Condition.wait(). Para llamar a wait() es obligatorio ser el propietario de
la Condition, cosa que se consigue llamando a acquire(). La llamada a wait() libera la Condition, pero deja al hilo
bloqueado hasta que alguien llame a Condition.notify().
El hilo encargado de suministrar los datos, debe llamar a Condition.acquire() para hacerse dueño de la Condition y cuando los datos estén disponibles,
llamar a Condition.notify() y luego aCondition.release().
Estas dos llamadas juntas despertarán al hilo a la espera de datos. La llamada
a notify() no libera la Condition, por lo que el hilo
que está en el wait() será notifiado, pero no comenzará su
ejecución hasta que se llame a release().
El ejemplo se muestra enseguida:
from threading import Thread, Condition import time class MiHilo (Thread): def __init__(self,lista,condicion): Thread.__init__(self) self.lista = lista self.condicion = condicion self.fin=False def run(self): self.condicion.acquire() while not self.fin: self.condicion.wait() if not self.fin: while len(lista)>0: print (self.lista.pop(0)) self.condicion.release() if __name__ == '__main__': lista =[] condicion = Condition() hilo = MiHilo(lista, condicion) hilo.start() for i in range (0,10): condicion.acquire() lista.append(i) lista.append("numero "+str(i)) condicion.notify() condicion.release() time.sleep(1) hilo.fin=True condicion.acquire() condicion.notify() condicion.release()
Eventos
La forma más fácil de hacer que un hilo espere a que otro hilo le
avise es por medio de Event.
El Event tiene un flag interno que indica si un hilo puede
continuar o no. Un hilo llama al método Event.wait()
y se queda bloqueado en espera hasta que el flag interno de Event se ponga a True. Otro hilo llame a Event.set() para poner el flag a True o bien a Event.clear() para ponerlo a False.
En realidad se trata
de un wrapper por encima de Condition y sirven principalmente para coordinar
threads mediante señales que indican que se ha producido un evento. Los eventos
nos abstraen del hecho de que estemos utilizando un Lock por debajo, por lo que
carece de métodos acquire y release. El ejemplo se muestra a continuación:
from threading import Thread, Event import time class MiHilo(Thread): def __init__(self, evento): Thread.__init__(self) self.evento=evento def run(self): self.evento.wait() print ("Entra hilo ") if __name__ == '__main__': evento = Event() hilo=MiHilo(evento) hilo.start() time.sleep(2) print ("Hago evento.set()") evento.set()
La información y los ejemplos se
obtuvieron de las siguientes fuentes:
Los ejemplos se pueden descargar
del siguiente link.