Back to Blog
Python

Python Lock vs RLock: Choosing the Right Threading Primitive

python lock vs rlock: Understand the difference between threading.Lock and threading.RLock in Python, including reentrancy, deadlock risks, and when each is appropriate.

threadingconcurrencylocksRLockdeadlock
Illustration comparing Python's Lock and RLock threading primitives, showing reentrancy and nested acquisition.

python lock vs rlock requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When writing multithreaded Python, the choice between threading.Lock and threading.RLock often comes down to whether a thread may need to acquire the same lock more than once. The difference is reentrancy: an RLock can be acquired multiple times by the same thread, while a Lock cannot. This distinction affects deadlock risk, code structure, and even performance in subtle ways.

The Core Difference Between Lock and RLock

A threading.Lock is a non-reentrant mutex. If a thread acquires it and then attempts to acquire it again before releasing it, the thread blocks forever because the lock is already held. This is a common source of deadlocks, especially when a function that acquires a lock calls another function that tries to acquire the same lock.

A threading.RLock (re-entrant lock) tracks both the owning thread and a recursion level. The same thread can acquire it multiple times, but each acquisition must be matched by a release. Other threads cannot acquire it until the owning thread releases it completely.

import threading lock = threading.Lock() rlock = threading.RLock()

How Lock Behaves in Nested Acquisition

Consider a class that uses a lock to protect its internal state. If one public method acquires the lock and then calls another method that also acquires the same lock, the second acquisition will deadlock with a plain Lock.

class Counter: def __init__(self): self.value = 0 self.lock = threading.Lock() def increment(self): with self.lock: self._update() def _update(self): with self.lock: # Deadlock! The lock is already held by this thread self.value += 1

Running this code will hang because the same thread tries to acquire a non-reentrant lock it already owns. The thread never reaches the release in the outer with block, so the program stalls.

How RLock Allows Reentrant Acquisition

Replacing the Lock with an RLock fixes the nested acquisition problem. The RLock records the owner thread and allows that thread to acquire it multiple times. Each acquire() increments an internal counter, and each release() decrements it. The lock is only released for other threads when the counter returns to zero.

class SafeCounter: def __init__(self): self.value = 0 self.lock = threading.RLock() def increment(self): with self.lock: self._update() def _update(self): with self.lock: # Safe: same thread can re-acquire self.value += 1

This pattern is common when you have a public API that delegates to internal helpers, and you want to keep the lock granularity consistent without exposing separate lock-acquisition logic.

Deadlock Risks and When RLock Helps

Using a plain Lock when reentrancy is needed can cause a deadlock that is difficult to debug because the stack trace shows the thread blocked on an acquire call that appears logically correct. The root cause is that the lock is not reentrant, not that two different threads are competing for the same resource.

RLock eliminates this class of deadlock, but it does not solve all deadlock problems. If two threads acquire multiple locks in different orders, an RLock will not prevent a classic deadlock. For example:

# Thread A with lock_a: with lock_b: ... # Thread B with lock_b: with lock_a: ...

Here, RLock does not help because the locks are different and each thread is waiting for the other. Reentrancy only addresses the case where the same thread needs to acquire the same lock again.

Performance and Overhead Considerations

RLock carries slightly more overhead than Lock because it must track the owning thread and a recursion count. In most applications this difference is negligible, but in highly contended, performance-critical code it can matter. If your code never needs reentrant acquisition, a plain Lock is the lighter choice.

More important than the micro-benchmark is the cost of a deadlock. A deadlock that hangs a production service is far more expensive than a few extra instructions per lock acquisition. When in doubt, choose the primitive that matches your actual usage pattern rather than optimizing for a tiny overhead difference.

Choosing Between Lock and RLock

Use a plain Lock when you are certain that a thread will never attempt to acquire the same lock more than once. This is common for simple critical sections that protect a single operation, such as updating a shared counter or appending to a list.

Use an RLock when your code has nested functions that all acquire the same lock, or when you are building a public API that calls internal methods that also lock. RLock is also useful when you want to allow a method to call itself recursively while holding the lock.

A practical rule: if you ever find yourself writing with self.lock: inside another with self.lock: block, switch to RLock. If you are not sure, start with Lock and only change to RLock if you hit a deadlock caused by reentrant acquisition.

Common Mistakes and Edge Cases

One common mistake is using an RLock when a plain Lock would be simpler, and then assuming it provides protection against all concurrency issues. RLock only protects against reentrant deadlocks, not against race conditions or inconsistent lock ordering.

Another edge case is mixing acquire() and release() calls with with statements. While with is the recommended style, manual calls must be balanced. For an RLock, the same thread must call release() as many times as it called acquire(). Failing to do so leaves the lock held indefinitely.

Finally, remember that RLock is owned by a thread. If a different thread tries to release it, a RuntimeError is raised. This is intentional and prevents one thread from accidentally releasing another thread's lock.

When you need to share a lock across threads, always document whether it is reentrant. This helps future maintainers understand why a nested with block is safe or why they must avoid calling certain methods while holding the lock.

python lock vs rlock: Practical Usage and Code Examples | RYUSLOG DEV