Skillversitas

Locks and Mutex

A lock (mutex) is the basic tool for making a read-modify-write sequence atomic: only one thread can hold the lock at a time, and every other thread that tries to acquire it blocks until it is released. The fix for the broken counter from the previous chapter is a three-line change.

Fixing the Counter

import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100_000):
        with lock:            # only one thread inside at a time
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(4)]
[t.start() for t in threads]
[t.join() for t in threads]
print(counter)  # always 400000 now

acquire and release

The with form is preferred because it releases the lock automatically even when the block raises. The manual form is useful when the critical section spans function calls:

lock.acquire()
try:
    update_state()
finally:
    lock.release()

Two extra variants exist on a lock: acquire(blocking=False) returns a boolean instead of waiting, and acquire(timeout=2) waits at most two seconds. Both are the basis of lock-free fallbacks: "try to take the lock, and if it is busy, do something else."

Reentrant Lock (RLock)

A Lock can only be acquired once per thread; a thread that tries to acquire it a second time deadlocks itself. An RLock (reentrant lock) may be acquired multiple times by the same thread, and is released only after that many releases. Use an RLock when a locked method calls another method that also takes the lock:

class BankAccount:
    def __init__(self, balance=0):
        self._balance = balance
        self._lock = threading.RLock()

    def deposit(self, amount):
        with self._lock:
            self._balance += amount

    def transfer(self, other, amount):
        with self._lock:                 # re-acquired by the same thread
            with other._lock:
                self._balance -= amount
                other.deposit(amount)    # fine: RLock allows re-entry

Scope Rules

  • Hold locks for the shortest correct region - never around I/O, time.sleep, or other threads' join().
  • Use one lock per piece of state, and document which lock protects which object.
  • Never create a lock inside a function that runs per call; the lock must outlive the critical sections.

Summary

Protect every shared read-modify-write with a threading.Lock used in a with block, prefer non-blocking or timed acquisition when a fallback exists, and reach for RLock only when the same thread must re-enter its own lock.