Welcome, guest | Sign In | My Account | Store | Cart

This is a class I made to make any object thread safe using a simple RLock. It makes the whole thing as transparent as possible, using __getattr__.

Python, 20 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class ThreadSafeObject:
    """
    A class that makes any object thread safe.
    """
    
    def __init__(self, obj):
        """
        Initialize the class with the object to make thread safe.
        """
        self.lock = threading.RLock()
        self.object = obj
        
    def __getattr__(self, attr):
        self.lock.acquire()
        def _proxy(*args, **kargs):
            self.lock.acquire()
            answer = getattr(self.object, attr)(*args, **kargs)
            self.lock.release()
            return answer
        return _proxy

I really don't like multi-threading, as it can cause a lot of subtle and hard to recreate bugs, race conditions and deadlocks... So when coworker asked me how to make a list thread safe, I wrote this little hack.

Some issues in this implementation is that the returned object doesn't have the __doc__ and other metainfo of the original class, but that can be added quite easily... I left it out to show the ease and elegance of the implementation... Python is powerful that way.

Sample usage - thread safeing a printer class:

class printer: def count(self, name): for i in range(50): print name, i

print " Not thread safe " x = printer() z = threading.Thread(target = x.count, args = ["2nd"]) z.start() x.count("1st")

print " Thread safe " x = ThreadSafeObject(printer()) z = threading.Thread(target = x.count, args = ["2nd"]) z.run() x.count("1st")

Output: Not thread safe 2nd 0 2nd 1 2nd 2 2nd 3 2nd 4 2nd 5 2nd 6 2nd 7 2nd 8 2nd 9 2nd 10 2nd 11 2nd 12 2nd 13 2nd 14 2nd 15 2nd 16 2nd 17 1st 0 1st 1 1st 2 1st 3 1st 4 1st 5 1st 6 1st 7 1st 8 1st 9 1st 10 1st 11 1st 12 1st 13 1st 14 1st 15 1st 16 1st 17 1st 18 1st 19 1st 20 1st 21 1st 22 1st 23 2nd 18 2nd 19 2nd 20 2nd 21 2nd 22 2nd 23 2nd 24 2nd 25 2nd 26 2nd 27 2nd 28 2nd 29 2nd 30 2nd 31 2nd 32 2nd 33 2nd 34 2nd 35 2nd 36 2nd 37 2nd 38 2nd 39 2nd 40 2nd 41 2nd 42 1st 24 1st 25 1st 26 1st 27 1st 28 1st 29 1st 30 1st 31 1st 32 1st 33 1st 34 1st 35 1st 36 1st 37 1st 38 1st 39 1st 40 1st 41 1st 42 1st 43 1st 44 1st 45 1st 46 1st 47 1st 48 2nd 43 2nd 44 2nd 45 2nd 46 2nd 47 2nd 48 2nd 49 1st 49 Thread safe 2nd 0 2nd 1 2nd 2 2nd 3 2nd 4 2nd 5 2nd 6 2nd 7 2nd 8 2nd 9 2nd 10 2nd 11 2nd 12 2nd 13 2nd 14 2nd 15 2nd 16 2nd 17 2nd 18 2nd 19 2nd 20 2nd 21 2nd 22 2nd 23 2nd 24 2nd 25 2nd 26 2nd 27 2nd 28 2nd 29 2nd 30 2nd 31 2nd 32 2nd 33 2nd 34 2nd 35 2nd 36 2nd 37 2nd 38 2nd 39 2nd 40 2nd 41 2nd 42 2nd 43 2nd 44 2nd 45 2nd 46 2nd 47 2nd 48 2nd 49 1st 0 1st 1 1st 2 1st 3 1st 4 1st 5 1st 6 1st 7 1st 8 1st 9 1st 10 1st 11 1st 12 1st 13 1st 14 1st 15 1st 16 1st 17 1st 18 1st 19 1st 20 1st 21 1st 22 1st 23 1st 24 1st 25 1st 26 1st 27 1st 28 1st 29 1st 30 1st 31 1st 32 1st 33 1st 34 1st 35 1st 36 1st 37 1st 38 1st 39 1st 40 1st 41 1st 42 1st 43 1st 44 1st 45 1st 46 1st 47 1st 48 1st 49

3 comments

Bertrand Croq 16 years, 3 months ago  # | flag

Metaclass. Wouldn't it be better to make it a metaclass instead of having to wrap the object into another object ?

Joe Jordan 16 years, 3 months ago  # | flag

Does not work, bad idea anyway. This recipe does not work at all, because it never releases the first lock acquired in ThreadSafeObject.__getattr__ (the RLock documentation at http://docs.python.org/lib/module-threading.html states that "the thread must release it once for each time it has acquired it").

The only reason it appears to work is because "z.run()" is used in the second example rather than "z.start()", so all the work is actually done sequentially in the main thread.

In general, you need to think about what and when you are locking rather than blindly wrapping single method calls with locks and hoping everything will work.

Also, the locks aren't released when exceptions occur. acquire() calls should almost always be followed by a try/finally: release() suite, or use the "with" statement introduced in Python 2.5, as per http://docs.python.org/lib/with-locks.html

Joe Jordan 16 years, 3 months ago  # | flag

For example. As a more concrete example of why it is a bad idea in general (assuming the leak in the recipe is removed), take the following code:

import Queue
import time

def consume(queue):
    for i in xrange(3):
        x = queue.get()
        print i, x

def produce(queue):
    for i in xrange(3):
        queue.put(i)
        time.sleep(1)

q = Queue.Queue()
#q = ThreadSafeObject(q)

t1 = threading.Thread(target=produce, args=(q,))
t2 = threading.Thread(target=consume, args=(q,))
t1.start()
t2.start()

This code is already thread-safe, and works fine. But uncomment the "q = ThreadSafeObject(q)" line and you have a deadlock.

Created by Mik Lala on Fri, 21 Dec 2007 (PSF)
Python recipes (4591)
Mik Lala's recipes (1)

Required Modules

  • (none specified)

Other Information and Tasks