Hoy aprenderás cómo los decoradores pueden hacer tu vida más fácil cuando manejas threads. Cualquiera que haya usado threads ha tenido que pelearse con sus problemas. Uno de los más importantes son las condiciones de carrera (race-conditions). Pero con los decoradores, esto puede ser fácilmente eliminado. Vamos a ver un ejemplo de código.
from threading import Thread import time import random criticalVar = 0 threads = 10 class MyThread (Thread): def __init__ (self, id): Thread.__init__ (self) self.TID = id # Thread id def run (self): global criticalVar print 'Thread [%d] starts' % self.TID localVar = criticalVar + 1 time.sleep (random.random ()) # Some long call between 0 and 1 criticalVar = localVar print 'Thread [%d] ends: criticalVar = %d' % (self.TID, criticalVar) if __name__ == '__main__': s = time.time () threadBag = [] for id in range (threads): t = MyThread (id) t.start () threadBag.append (t) for thread in threadBag: thread.join () print 'Total time = %f' % (time.time () - s)
Este código muestra un ejemplo típico de threads. Si lanzas este ejemplo, mostrará:
Thread [0] starts Thread [1] starts Thread [2] starts Thread [3] starts Thread [4] starts Thread [5] starts Thread [6] starts Thread [7] starts Thread [8] starts Thread [9] starts Thread [0] ends: criticalVar = 1 Thread [2] ends: criticalVar = 1 Thread [3] ends: criticalVar = 1 Thread [9] ends: criticalVar = 1 Thread [4] ends: criticalVar = 1 Thread [5] ends: criticalVar = 1 Thread [7] ends: criticalVar = 1 Thread [8] ends: criticalVar = 1 Thread [1] ends: criticalVar = 1 Thread [6] ends: criticalVar = 1 Total time = 0.872000
La salida muestra una preciosa condición de carrera en criticalVar ya que todos los threads leen la variable, hacen algo que consume tiempo y la escriben después de que todos la hayan leido. Este comportamiento lleva a un fallo porque la función run() no es segura para threads (thread-safety).
Pero, con un pequeño decorador y alguna magía de locks…
from threading import Thread, Lock import time import random criticalVar = 0 threads = 10 myLock = Lock () def sync (lock): def function (f): def wrapper (*args, **kargs): lock.acquire () try: return f(*args, **kargs) finally: # exec in all cases lock.release () return wrapper return function class MyThread (Thread): def __init__ (self, id): Thread.__init__ (self) self.TID = id # Thread id @sync (myLock) def run (self): global criticalVar print 'Thread [%d] starts' % self.TID localVar = criticalVar + 1 time.sleep (random.random ()) # Some long call between 0 and 1 criticalVar = localVar print 'Thread [%d] ends: criticalVar = %d' % (self.TID, criticalVar) if __name__ == '__main__': s = time.time () threadBag = [] for id in range (threads): t = MyThread (id) t.start () threadBag.append (t) for thread in threadBag: thread.join () print 'Total time = %f' % (time.time () - s)
Ésta es la versión segura. Con el decorador al principio de la función run(), cada thread tiene que adquirir el lock y liberarlo al terminar. Por lo que el decorador hace la función run() segura ant threads y la salida queda:
Thread [0] starts Thread [0] ends: criticalVar = 1 Thread [1] starts Thread [1] ends: criticalVar = 2 Thread [2] starts Thread [2] ends: criticalVar = 3 Thread [3] starts Thread [3] ends: criticalVar = 4 Thread [4] starts Thread [4] ends: criticalVar = 5 Thread [5] starts Thread [5] ends: criticalVar = 6 Thread [6] starts Thread [6] ends: criticalVar = 7 Thread [7] starts Thread [7] ends: criticalVar = 8 Thread [8] starts Thread [8] ends: criticalVar = 9 Thread [9] starts Thread [9] ends: criticalVar = 10 Total time = 3.315000
Esta salida sólo tiene un problema: Total time. Con la adición del decorador, el script se comporta de forma secuencial en vez de paralela, pero eso es una efecto conocido de la seguridad de threads. En cualquier caso, el script corre correctamente, y eso es lo más importante.
Para más información sobre Threads y Decoradores.
Esto es todo por ahora
. Los comentarios son bienvenidos.