Python Central HubSign in

Python Programming / Python Synchronization

Locks (Lock vs RLock)

Prevent race conditions with Lock, and learn when to use a re-entrant lock (RLock).

Lock

A Lock allows only one thread to enter a critical section at a time.

Python
import threading
 
lock = threading.Lock()
count = 0
 
 
def add():
    global count
    for _ in range(100_000):
        with lock:
            count += 1
 
 
t1 = threading.Thread(target=add)
t2 = threading.Thread(target=add)
 
t1.start(); t2.start()
t1.join(); t2.join()
 
print(count)

What is a “critical section”?

A piece of code that must not be interrupted by another thread while it updates shared data.

RLock (re-entrant lock)

An RLock can be acquired multiple times by the same thread.

Use it when:

  • a function that holds a lock calls another function that tries to acquire the same lock
Python
import threading
 
lock = threading.RLock()
 
 
def outer():
    with lock:
        inner()
 
 
def inner():
    with lock:
        print("safe")
 
 
t = threading.Thread(target=outer)
t.start(); t.join()
sketch A Lock will deadlock against itself; an RLock will not p5.js
One thread, one lock, acquired twice. A plain Lock has no idea who holds it -- the second acquire blocks forever against a lock the SAME thread is holding, which is a deadlock with only one thread involved. An RLock records its owner and a count, so the owner may re-enter. Measured: the second acquire returns False on a Lock and True on an RLock. The catch is the release count -- an RLock stays held until you release it as many times as you took it.

Best practices

  • Keep with lock: blocks short.
  • Always acquire locks in a consistent order if using multiple locks.

Why a lock is needed

Even a line as small as count += 1 is really three steps: read the value, add one, and write it back. If two threads interleave those steps, both may read the same old value and one increment is lost. This is a race condition, and it produces wrong answers that change from run to run, which makes it hard to reproduce. A lock guarantees that only one thread executes the protected steps at a time.

Reading the Lock example

Each thread does 100,000 increments inside with lock:. The with statement acquires the lock on entry and releases it on exit, even if an exception occurs, so you cannot forget to release. Because the increments cannot overlap, the final count is always exactly 200000. Remove the lock and the total can come out lower.

A Lock is not tied to a thread and does not care who holds it. That is the source of a classic trap: if a thread tries to acquire a plain Lock it already holds, it waits for itself forever.

Why RLock exists

An RLock (re-entrant lock) remembers which thread owns it and how many times it has been acquired. The same thread can acquire it again without blocking, and it is released for others only after the matching number of releases. This solves the case in the example, where outer() holds the lock and calls inner(), which needs it too. Try changing RLock to Lock there and the program hangs.

A realistic use: a class that protects its own data

Python
import threading
 
 
class SafeCounter:
    def __init__(self) -> None:
        self._lock = threading.RLock()
        self.value = 0
 
    def increment(self) -> None:
        with self._lock:
            self.value += 1
 
    def increment_twice(self) -> None:
        with self._lock:          # takes the lock...
            self.increment()      # ...and increment() takes it again: fine with RLock
            self.increment()
 
 
counter = SafeCounter()
threads = [threading.Thread(target=counter.increment_twice) for _ in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(counter.value)  # 20

Common mistakes

  • Using two different lock objects for the same data. Threads must share one lock to exclude each other.
  • Calling acquire() without release() on an error path. Prefer with.
  • Locking too much. Long critical sections make threads wait and remove the benefit of threading.
  • Reaching for RLock by default. It can hide design problems; use a plain Lock unless re-entry is truly needed.

When to use which

Use Lock to protect shared data that several threads change. Use RLock when methods that hold the lock call other methods that also need it.

🧪 Try It Yourself

Exercise 1 – Basic Lock Usage

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 2 – Lock as Context Manager

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 3 – Detect Locked State

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Was this page helpful?

Let us know how we did