Python Threads: Locks and RLocks

Threads are like busy helpers working together. Sometimes, they need to share the same tools — like a hammer or a paintbrush. If two helpers try to use the same tool at once, things get messy!

Locks act like a tool locker that lets only one helper use a tool at a time, keeping things safe and organized. But what if a helper needs the same tool more than once? That’s where RLocks come in — they let the same helper grab the tool multiple times without getting stuck.

In this article, we’ll learn how to use Locks and RLocks in Python with simple and fun examples, so your threads can work together smoothly.

Locks and RLocks

A Lock is the basic tool to keep things safe when multiple threads want to use the same data. It works like a single key — only one thread can hold the lock at a time, so others have to wait their turn. This prevents threads from stepping on each other’s toes and messing up shared information.

An RLock, or reentrant lock, is a special kind of lock that allows the same thread to grab it multiple times without getting stuck. This is useful because sometimes a thread might call a function that tries to lock again while it already holds the lock. With a normal Lock, this would cause the thread to wait on itself forever (a deadlock), but an RLock lets it safely lock multiple times and only releases when all locks are released.

In short, use a Lock for simple, one-time access control, and an RLock when your code structure might require the same thread to lock a resource more than once.

Creating and Using a Lock

To create a Lock, simply use threading.Lock(). This gives you a lock object that you can use to control access to shared data. The best way to use a Lock is with the with lock: statement. This way, the lock is automatically acquired when you enter the block and released when you leave, even if an error happens.

Here’s a fun example: Two threads try to increase the same shared number, count, many times. By using a Lock, we make sure only one thread changes count at a time, keeping the number accurate.

import threading

lock = threading.Lock()
count = 0

def add():

    global count

    for _ in range(100000):
        with lock:
            count += 1

t1 = threading.Thread(target=add)
t2 = threading.Thread(target=add)
t1.start()
t2.start()
t1.join()
t2.join()

print(f"Count with Lock: {count}")

Without the lock, the count might be wrong because threads could update count at the same time. Using the lock makes sure everything happens safely and correctly.

What Happens Without Locks

When you don’t use a lock, threads can step on each other’s toes while changing shared data. Here’s a quick example where two threads try to increase the same number without any lock. The final count will often be wrong because both threads may update the number at the same time, causing mistakes.

import threading

count = 0

def add():

    global count

    for _ in range(100000):
        count += 1

t1 = threading.Thread(target=add)
t2 = threading.Thread(target=add)
t1.start()
t2.start()
t1.join()
t2.join()

print(f"Count without Lock: {count}")

It’s like two chefs reaching for the same ingredient at once — instead of working smoothly, they bump into each other, making a mess! Locks help avoid this chaos by making sure only one chef uses the ingredient at a time.

Using RLock: Why and How

Sometimes, a thread needs to lock the same resource more than once before releasing it. This happens often with recursive functions or when a function calls another function that uses the same lock. If you use a regular Lock here, the thread would get stuck waiting for a lock it already holds. That’s why we have RLock — a reentrant lock that lets the same thread acquire it multiple times safely.

Here’s a fun example of a recursive function grabbing the same RLock several times. Each time it acquires the lock, it prints a message, then calls itself with a smaller number. Because it uses RLock, there’s no deadlock, and the thread can lock repeatedly without problem.

import threading

rlock = threading.RLock()

def recursive_task(n=2):

    with rlock:

        print(f"Lock acquired, n={n}")

        if n > 0:
            recursive_task(n-1)

t = threading.Thread(target=recursive_task)
t.start()
t.join()

This example shows how the same thread safely acquires the lock multiple times as it goes deeper into the recursion. When the recursive calls return, the lock is released step by step. This wouldn’t work with a normal Lock, which doesn’t allow re-acquisition by the same thread.

In summary, use a regular Lock when simple exclusive access is needed. But when your code might re-enter locked sections—like recursive calls or nested functions—switch to an RLock to keep everything running smoothly without getting stuck.

Difference Between Lock and RLock

FeatureLockRLock
Can same thread acquire multiple times?❌ No✅ Yes
Use caseSimple exclusive accessWhen recursive locking needed
Behavior on re-acquireDeadlock (blocks forever)Works fine

This table shows the main difference clearly: a Lock is simple and strict — if the same thread tries to lock again, it will freeze (deadlock). An RLock, on the other hand, lets the same thread grab the lock multiple times safely. Use RLock when your thread might re-enter locked code, like in recursive or nested calls.

Locking Custom Objects with RLock

Here, we use an RLock inside a class to keep things safe when methods call each other. The Counter class has two methods, increment and double_increment. Both lock the RLock before changing the count. Because it’s an RLock, the same thread can lock it twice safely — when increment calls double_increment — without causing a deadlock.

import threading

class Counter:

    def __init__(self):
        self.count = 0
        self.lock = threading.RLock()

    def increment(self):

        with self.lock:
            self.count += 1
            self.double_increment()

    def double_increment(self):

        with self.lock:
            self.count += 1

counter = Counter()
threads = [threading.Thread(target=counter.increment) for _ in range(5)]

for t in threads:
    t.start()

for t in threads:
    t.join()

print(f"Final count: {counter.count}")

This way, each thread can safely call increment, which internally calls double_increment, all without getting stuck. The RLock keeps the shared count safe while allowing nested locking.

Tips for Using Locks and RLocks

When using locks, keep the locked sections short and focused to avoid confusion. This makes your code easier to read and understand.

Always use the with statement to handle locks — it automatically acquires and releases the lock safely, even if something goes wrong inside the block.

Use RLock only when you know the same thread might need to acquire the lock multiple times, like when methods call each other or in recursive functions. Otherwise, a simple Lock is enough.

Conclusion

Locks help your threads avoid stepping on each other’s toes by making sure only one thread uses shared data at a time. RLocks give extra safety when the same thread needs to lock multiple times, like in nested calls.

Give Locks and RLocks a try in your own multi-threaded programs to keep data safe — and keep things running smoothly without chaos!