Hilos en python

De ChuWiki
Saltar a: navegación, buscar

En python tenemos dos módulos que nos permiten tratar con hilos. El módulo thread es el de más bajo nivel y tiene algunas funciones que nos permiten trabajar con hilos. El módulo threading es de más alto nivel y tiene la clase Thread, que representa un hilo. Vamos a ver algunos ejemplos con ambos módulos.


Módulo thread

Arrancando un hilo

Para arrancar un hilo, tenemos la función start_new_thread() del módulo thread. A esta función únicamente debemos pasarle la función python que queremos que se ejecute en un hilo y la tupla que hará de parámetros de dicha función. Dicho de otra forma, si definimos la función

def funcion (inicio, fin):

y queremos arrancarla como un hilo, pasándole, por ejemplo, un 3 y un 11 como parámetros de inicio y fin, haremos la llamada a la función start_new_thread() de la siguiente forma

import thread
...
thread.start_new_thread(funcion, (3,11))

y eso es todo, la función comenzará a ejecutarse en un hilo separado y hará lo que codifiquemos en ella. Como ejemplo, haremos una función que cuente desde inicio a fin, de uno en uno, esperando 0.1 segundos entre cuenta y cuenta. El main lanzará el hilo y se pondrá a su vez a contar. Debemos ver en pantalla como van contando en paralelo el main y el hilo. El código de la función puede ser este

import thread
import time

# Indica si ha terminado el hilo
hilo_terminado = False

# Funcion que se ejecuta en un hilo
def funcion (inicio, fin):
    global hilo_terminado
    # Bucle de inicio a fin, con espera de un segundo
    for valor in range(inicio,fin):
        print "Hilo : "+str(valor)
        time.sleep(0.1)
    # marca hilo terminado
    hilo_terminado = True
    print "Terminado hilo "

Hemos definido una variable global hilo_terminado a False. El hilo, cuando termina, la pone a True. Más adelante veremos el motivo. El resto del código es sencillo: un simple bucle con una espera de 0.1 segundos.

Ahora, el main, lanzará el hilo y se pondrá a contar a su vez

if __name__ == '__main__':
    # Lanzamos un hilo
    thread.start_new_thread(funcion, (3,11))
    # y nos ponemos a contar, en paralelo con lo que haga
    # el hilo
    for i in range(1,5):
        print "Principal : "+str(i) 
        time.sleep(0.1)
    # Espera a que termine el hilo
    while (not hilo_terminado):
        pass

Para lanzar el hilo, hacemos la llamada a start_new_thread(). Luego el bucle y al final viene un pequeño tema importante. Es necesario que el main espere a que los hilos arrancados terminen, ya que si no lo hace, el ejecutable python terminará y los hilos que queden arrancados se abortarán inmediatamente. Ya que habíamos puesto una variable booleana hilo_terminado que se ponía a True cuando el hilo terminaba, en el main sólo tenemos que hacer un bucle de espera a que esta variable se ponga a True. La función pass de python es una función que sólo sirve para perder el tiempo, así que es buena para meterla dentro de un bucle de espera.

Puedes ver el código completo en thead_hilo_simple.py


Espera de unos hilos por otros

Si necesitamos que unos hilos esperen por otros, python en su módulo thread nos ofrece la función thread.allocate_lock() que nos devolverá un objeto de tipo thread.LockType. Los hilos deben pedir a este objeto permiso para hacer su tarea y liberarlo cuando quieran dar paso a otros hilos. Para ello, llaman a los método acquire() y release()

import thread
...
lock = thread.allocate_lock()
...
lock.acquire()
# Aqui el hilo realiza su trabajo
lock.release()

Mientras el hilo esté entre el acquire() y el release(), cualquier otro hilo se quedará bloqueado en la llamada a acquire() de ese mismo lock.

Vamos a hacer un pequeño ejemplo usando todo esto. El main creará primero un LockType y lo bloqueará. Luego arrancará un hilo que se quedará esperando por dicho lock. El main, pasado dos segundos, dará permiso al hilo para continuar. Aprovecharemos el lock para cambiar el bucle de espera del ejemplo anterior. En vez de un bucle, usaremos el lock para esperar que el hilo termine. El código del main puede ser parecido a este

if __name__ == '__main__':
    # Pedimos un lock y lo bloqueamos
    bloqueo = thread.allocate_lock()
    bloqueo.acquire()
    # Lanzamos un hilo, que se quedara en espera. Pasamos
    # como parametros un par de valores inventados para el hilo
    # y debemos tambien pasar el lock.
    thread.start_new_thread(funcion, (3,11,bloqueo))
    # Hacemos una espera de dos segundos para asegurarnos que
    # entra el hilo
    time.sleep(2)
    #Liberamos el bloqueo
    print "Principal: Libero bloqueo"
    bloqueo.release()
    # Espera a que termine el hilo, esperando la liberacion
    # del bloqueo
    bloqueo.acquire()
    bloqueo.release()
    print "Fin de programa"

En cuanto al hilo, será una funcion() que esperará el lock para escribir en pantalla los dos valores recibidos como parámetro. Luego liberará el lock. El código es este

def funcion (inicio, fin, bloqueo):
    global hilo_terminado
    # El hilo espera el bloqueo
    print "Hilo: Pido bloqueo"
    bloqueo.acquire()
    print "Hilo: Empiezo "+str(inicio)+" "+str(fin)
    hilo_terminado = True
    print "Hilo: y termino"
    bloqueo.release()

Tienes el código completo en thread_hilo_lock.py

Módulo threading

Un hilo simple

Un módulo de más alto nivel que nos permite hacer hilos en python es threading. Este módulo, entre otras cosas, tiene una clase Thread, que es la que representa el hilo. Para hacer un hilo, debemos heredar de ella y definir el método run(). Lo que pongamos en ese método se ejecutará en un hilo aparte. Para arrancar el hilo, debemos instanciar la clase hija de Thread que hayamos hecho y llamar a su método start(). Los pasos básicos con estos

from threading import Thread

class MiHilo(Thread):
   def run():
      #Aqui el codigo del hilo
...
# Arranque del hilo
hilo = MiHilo()
hilo.start()

Si a la clase MiHilo le ponemos un constructor por el motivo que sea (queremos recibir parámetros, por ejemplo), debemos llamar explícitamente al constructor de la clase padre Thread.

class MiHilo(Thread):
   def __init__(self, parametros):
      Thread.__init__(self)
      ...

Al usar estos hilos y a diferencia del hilo del módulo thread, no es necesario que el main espere que los hilos terminen. El main puede terminar y el programa seguirá vivo hasta que todos los hilos terminen.

En threading_hilo_simple.py tienes un ejemplo completo. Se crea una clase MiHilo hija de Thread que recibe en el constructor dos valores inicio y fin. En el metodo run(), un bucle desde inicio hasta fin con una espera en cada iteración. Por su parte, el main crea el hilo, lo arranca y se pone a su vez a contar, más rápido para terminar antes.


Lock y RLock

Dentro del módulo threading, tenemos las clase Lock y RLock para el bloqueo de hilos y hacer que unos esperen por otros (por ejemplo, para acceder a un recurso compartido).

Lock es la clase más simple. 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

bloqueo = Lock()
...
# un hilo hace
bloqueo.acquire()
print "hago cosas
...
# otro hilo hace
bloqueo.acquire() # y se queda en espera del primer hilo
...
# cuando el primer hilo termina, hace
bloqueo.release() # y el segundo hilo comienza sus tareas
...
# cuando el segundo hilo termina, hace
bloqueo.release()

Esta clase Lock es muy simple. Cualquier hilo, haya sido él el que ha bloqueado el Lock o no, puede liberarlo. Si un hilo llama él mismo dos veces a acquire(), se queda bloqueado en la segunda llamada.

Para sincronización más compleja, tenemos también RLock. Este lock sí tiene en cuenta quién es el propietario del bloqueo, de forma que sólo puede release() el hilo que haya hecho el acquire(). También tiene en cuenta el número de llamadas a acquire(). 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.

Tienes dos ejemplos sencillos con ambos tipos de lock en threading_hilo_lock.py y threading_hilo_rlock.py


Condition

Uno de los usos habituales de los hilos 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 a Condition.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 resumen de esto puede ser el siguiente. En algún lado se crea la Condition

condicion = Condition()

El hilo que está a la espera de datos, debe hacer algo como esto

condicion.acquire()
condicion.wait() # Espera por los datos
# Tratamiento de los datos
condicion.release()

Mientras que el hilo que genera los datos, debe hacer esto

# Generar los datos
condicion.acquire()
condicion.notify()
condicion.release()

En threading_hilo_condition.py tienes un ejemplo completo en el que el main y un hilo comparten una Condition y una lista de valores. El main va metiendo cada segundo un par de valores y notificando al hilo. El hilo está a la espera de esa notificación para sacar los valores de la lista y mostrarlos por pantalla.


Semaphore

Uno de los mecanismos más antiguos de sincronización de hilos son los 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.

Los semáforos sirven para permitir el acceso a un recurso que admite un número máximo de hilos simultáneos. Por ejemplo, si cada hilo abre su conexión a base de datos y sólo queremos un máximo de cinco conexiones abiertas simultáneamente, un semáforo puede ser una opción.

En el código, se debe crear el semáforo indicando el valor inicial del contador (número máximo de hilos que pueden estar activos simultáneamente)

from threading import Semaphore
...
semaforo = Semaphore(5)

Y luego, cada hilo, debe hacer lo siguiente

semaforo.acquire()
# Aqui el hilo realiza su trabajo
semaforo.release()

Tienes un ejemplo completo en threading_hilo_semaphore.py


Event

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 el código, símplemente se instancia el Event

evento = Event()

El hilo que tenga que esperar símplemente llama a wait()

evento.wait()
# Aqui el hilo hace sus cosas

y el hilo que tenga que despertar al anterior, le basta con llamar a set()

event.set()
# El hilo anterior despierta

Tienes el ejemplo completo en threading_hilo_event.py