When and how to use Python's RLock

Awalias picture Awalias · May 15, 2013 · Viewed 30.2k times · Source

Reading through the Python docs I came across RLock.

Can someone explain to me (with example) a scenario in which RLock would be preferred to Lock?

With particular reference to:

  • RLock's “recursion level”. How is this useful?
  • A threads "ownership" of an RLock object
  • Performance?

Answer

User picture User · May 15, 2013

This is one example where I see the use:

Useful when

  1. you want to have thread-safe access from outside the class and use the same methods from inside the class:

    class X:
        def __init__(self):
            self.a = 1
            self.b = 2
            self.lock = threading.RLock()
    
        def changeA(self):
            with self.lock:
                self.a = self.a + 1
    
        def changeB(self):
            with self.lock:
                self.b = self.b + self.a
    
        def changeAandB(self):
            # you can use chanceA and changeB thread-safe!
            with self.lock:
                self.changeA() # a usual lock would block at here
                self.changeB()
    
  2. for recursion more obvious:

    lock = threading.RLock()
    def a(...):
         with lock:
    
             a(...) # somewhere inside
    

    other threads have to wait until the first call of a finishes = thread ownership.

Performance

Usually, I start programming with the Lock and when case 1 or 2 occur, I switch to an RLock. Until Python 3.2 the RLock should be a bit slower because of the additional code. It uses Lock:

Lock = _allocate_lock # line 98 threading.py

def RLock(*args, **kwargs):
    return _RLock(*args, **kwargs)

class _RLock(_Verbose):

    def __init__(self, verbose=None):
        _Verbose.__init__(self, verbose)
        self.__block = _allocate_lock()

Thread Ownership

within the given thread you can acquire a RLock as often as you like. Other threads need to wait until this thread releases the resource again.

This is different to the Lock which implies 'function-call ownership'(I would call it this way): Another function call has to wait until the resource is released by the last blocking function even if it is in the same thread = even if it is called by the other function.

When to use Lock instead of RLock

When you make a call to the outside of the resource which you can not control.

The code below has two variables: a and b and the RLock shall be used to make sure a == b * 2

import threading
a = 0 
b = 0
lock = threading.RLock()
def changeAandB(): 
    # this function works with an RLock and Lock
    with lock:
        global a, b
        a += 1
        b += 2
        return a, b

def changeAandB2(callback):
    # this function can return wrong results with RLock and can block with Lock
    with lock:
        global a, b
        a += 1
        callback() # this callback gets a wrong value when calling changeAandB2
        b += 2
        return a, b

In changeAandB2 the Lock would be the right choice although it does block. Or one can enhance it with errors using RLock._is_owned(). Functions like changeAandB2 may occur when you have implemented an Observer pattern or a Publisher-Subscriber and add locking afterward.