Vasu Patel logoVasu Patel
← Sharing Time: Concurrency & Multithreading
All chapters in this track
  1. 01Chapter 1: Concurrency vs. Parallelism, and Why It's Hard
  2. 02Chapter 2: Processes vs. Threads, Revisited for Concurrency
  3. 03Chapter 3: The Core Hazards: Race Conditions, Deadlock, Livelock, Starvation
  4. 04Chapter 4: Synchronization Primitives: Locks, Semaphores, Monitors, Condition Variables
  5. 05Chapter 5: Designing for Thread Safety: Immutability, Confinement, Thread-Safe Collections
  6. 06Chapter 6: Classic Patterns: Producer-Consumer, Reader-Writer, Thread Pools
  7. 07Chapter 7: Higher-Level Models: Async/Await, Event Loops, Coroutines, Actors, Channels
  8. 08Chapter 8: Memory Models and Visibility: Happens-Before, Volatile, Atomics
  9. 09Chapter 9: Lock-Free and Wait-Free Programming Basics
  10. 10Chapter 10: How Concurrency Differs Across Runtimes: JVM Threads, Python's GIL, Go's Goroutines, JavaScript's Event Loop
  11. 11Chapter 11: Debugging and Testing Concurrent Code
  12. 12Chapter 12: Classic Interview Problem: Producer-Consumer
  13. 13Chapter 13: Classic Interview Problem: Dining Philosophers
  14. 14Chapter 14: Classic Interview Problem: Readers-Writers
  15. 15Chapter 15: Concurrency in System Design and Production Debugging

Chapter 4 of 15

Chapter 4: Synchronization Primitives: Locks, Semaphores, Monitors, Condition Variables

Why this matters

Chapter 3 showed a lock fixing a race condition, but "use a lock" is only the beginning of the synchronization toolkit — different coordination problems call for genuinely different primitives, and using the wrong one (or the right one incorrectly) is a common source of subtle bugs. This chapter builds the full vocabulary precisely.

Locks and mutexes

[BEGINNER] A mutex (mutual exclusion lock) allows only one thread to hold it at a time — any other thread attempting to acquire it blocks until it's released. Python's threading.Lock is exactly this.

import threading

lock = threading.Lock()
shared_data = []

def add_item(item):
    with lock:  # acquire on entry, release on exit -- even if an exception occurs
        shared_data.append(item)

[SENIOR INSIGHT] Python also provides threading.RLock (a reentrant lock) — a lock that the same thread can acquire multiple times without deadlocking itself, tracking an internal acquisition count and only truly releasing when that count returns to zero. This matters specifically when a locked method might call another locked method on the same object from the same thread (e.g., a method holding the lock calling another method that also tries to acquire it) — a plain Lock would deadlock the thread against itself in this situation; an RLock handles it correctly.

class Account:
    def __init__(self):
        self._lock = threading.RLock()
        self._balance = 0

    def deposit(self, amount):
        with self._lock:
            self._balance += amount
            self._log_transaction("deposit", amount)  # this ALSO acquires self._lock

    def _log_transaction(self, kind, amount):
        with self._lock:  # SAME thread re-acquiring -- fine with RLock, would deadlock with plain Lock
            print(f"{kind}: {amount}")

Semaphores: limiting concurrent access to N

[BEGINNER] A semaphore generalizes a lock: instead of allowing exactly one holder, it allows up to N concurrent holders, tracked via an internal counter. A mutex is really just a semaphore with N=1.

import threading

# Limit to 3 concurrent connections to an external service, regardless of how
# many threads WANT to connect at once.
connection_semaphore = threading.Semaphore(3)

def call_external_service(request_id):
    with connection_semaphore:  # blocks if 3 are already in use; proceeds once one frees up
        print(f"Request {request_id} is using a connection")
        # ... do the work ...

When to use it: limiting concurrent access to a genuinely limited resource (a connection pool, a fixed number of worker slots, rate-limiting concurrent API calls) — anywhere "at most N at a time" is the actual requirement, rather than "exactly one at a time."

Monitors: a lock plus its protected data, bundled together

[BEGINNER] A monitor is a design pattern (not always a distinct language construct) that bundles a lock together with the specific data it protects, and the operations that need that protection — ensuring the lock is always acquired whenever the protected data is touched, structurally, rather than relying on every caller remembering to lock manually.

class MonitoredCounter:
    """A monitor: the lock and the data it protects are bundled into ONE object,
    so it's structurally impossible to touch _value without going through
    a method that acquires the lock first."""
    def __init__(self):
        self._lock = threading.Lock()
        self._value = 0

    def increment(self):
        with self._lock:
            self._value += 1

    def get(self):
        with self._lock:
            return self._value

[IN PRACTICE] This is directly the companion OOP book's Chapter 3 encapsulation principle, applied specifically to concurrent state: by making _value genuinely private and only touchable through methods that always acquire the lock first, it becomes structurally impossible for any code to accidentally access the protected data without synchronization — the same "structural prevention over convention" theme that recurs throughout this entire book series.

Condition variables: waiting for a specific state, efficiently

[BEGINNER] A condition variable lets a thread wait (releasing its lock while waiting, so other threads can make progress) until another thread signals that some specific condition has changed — solving the problem of "I need to wait until X is true, without busy-polling in a loop wasting CPU."

import threading

class BoundedBuffer:
    def __init__(self, capacity):
        self._lock = threading.Lock()
        self._not_full = threading.Condition(self._lock)
        self._not_empty = threading.Condition(self._lock)
        self._items = []
        self._capacity = capacity

    def put(self, item):
        with self._not_full:
            while len(self._items) >= self._capacity:
                self._not_full.wait()  # releases the lock, sleeps until notified -- NOT busy-polling
            self._items.append(item)
            self._not_empty.notify()  # wake up one thread waiting in get()

    def get(self):
        with self._not_empty:
            while len(self._items) == 0:
                self._not_empty.wait()
            item = self._items.pop(0)
            self._not_full.notify()  # wake up one thread waiting in put()
            return item

[SENIOR INSIGHT] Notice the while loop around wait(), not an if — this is deliberate and important, called guarding against spurious wakeups. A thread can wake from wait() for reasons other than the specific condition actually becoming true (another thread's notify() that turns out to be for a different situation, or platform-level spurious wakeups), so the condition must always be re-checked after waking, in a loop, rather than assumed true just because wait() returned. This is a genuinely common, subtle bug source when engineers use if instead of while around a condition variable wait.

Choosing the right primitive

NeedPrimitive
Exactly one thread accessing a resource at a timeMutex/Lock
Up to N threads accessing a resource at a timeSemaphore
A lock bundled with the data it protects, always enforcedMonitor (a pattern, built from a Lock)
A thread needs to wait efficiently for a specific condition to become trueCondition variable

Common mistakes

  1. Using a plain Lock where the same thread might need to re-acquire it (a method calling another locked method on itself), causing a self-deadlock — use RLock when this reentrant pattern is genuinely needed.
  2. Using if instead of while around a condition variable's wait() call, missing the need to re-check the condition after a spurious or stale wakeup.
  3. Using a semaphore with N=1 when a plain, simpler Lock would communicate intent more clearly — reserve semaphores specifically for genuine "up to N concurrent" requirements.
  4. Not bundling a lock with its protected data as a monitor, leaving it to convention (a comment saying "remember to lock before touching this") rather than structural enforcement — exactly the encapsulation lesson this book keeps returning to.

Discussion / interview questions

  1. "What's the difference between a mutex and a semaphore?" Strong answer: A mutex allows exactly one holder at a time; a semaphore generalizes this to allow up to N concurrent holders, tracked via an internal counter — a mutex is effectively a semaphore with N=1.

  2. "When would you need a reentrant lock (RLock) instead of a plain lock?" Strong answer: When the same thread might need to acquire the same lock multiple times without releasing it in between (e.g., a locked method calling another locked method on the same object) — a plain lock would deadlock the thread against itself; an RLock tracks acquisition count per-thread and handles this correctly.

  3. "Why should a condition variable's wait() call always be inside a while loop checking the condition, rather than an if statement?" Strong answer: A thread can wake from wait() due to a spurious wakeup or a notification meant for a different situation, without the actual condition being true yet — the condition must be re-checked after waking, in a loop, to guard against proceeding incorrectly on a false wakeup.

  4. "What is a monitor, and how does it relate to encapsulation?" Strong answer: A monitor bundles a lock together with the specific data it protects and the operations that need that protection, making it structurally impossible to access the protected data without going through the lock — directly analogous to the OOP principle of encapsulation, applied to concurrent state specifically.

Chapter summary / cheat sheet

  • Mutex/Lock: exactly one holder at a time. Semaphore: up to N holders, generalizing a mutex (N=1).
  • RLock: reentrant — the same thread can safely re-acquire it, tracked by an internal count; needed when a locked method calls another locked method on the same object from the same thread.
  • Monitor: a design pattern bundling a lock with its protected data, structurally enforcing synchronized access — direct concurrency-specific application of encapsulation.
  • Condition variable: lets a thread wait efficiently for a specific condition, without busy-polling — always re-check the condition in a while loop after waking, to guard against spurious wakeups.