Hi all, in this post you’ll learn how decorators can make your life easier when dealing with threads. Anyone that has experience with threads also have experience with its troubles. One of the most important troubles with threads are race-conditions. But with decorators, this can be easily throw away. Let’s look to an example of code:
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)
This code shows a typical threading example. If you run this example it will show:
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
The output prints a pretty race-condition in the criticalVar because all threas reads the variable, make something time-consuming and then writes the variable. This behaviour leads to a failure because of the not thread-safety function run().
But, with a little decorator and some lock magic…
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)
This is a thread-safe version. With the decorator at the begin of the run() function, each thread has to acquire the lock and release it when it finish. So, this decorator makes run() thread safe and the output is:
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
This output only has one problem: Total time. Since thread-safe decorator, script behaves sequential instead of parallel, but it’s a known effect of thread-safety. Any case, script runs correctly, and this is the most important thing.
For further information about Threads and Decorators.
This is all for now
. Comments welcomed.