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 2 of 15

Chapter 2: Processes vs. Threads, Revisited for Concurrency

Why this matters

The companion Operating System book covered processes and threads as OS-level constructs — isolated memory versus shared memory, and their respective context-switch costs. This chapter revisits that same distinction specifically through a concurrency lens: what does shared memory actually mean for the hazards this book covers, and why does Python's process model (multiprocessing) sidestep some concurrency problems that its thread model doesn't.

The core concept, restated for this book's purposes

[BEGINNER] Recall from the OS book: threads within a process share the same memory address space; processes have isolated memory. For concurrency specifically, this single fact has an enormous consequence: race conditions require shared mutable state to even be possible. Two threads can race over the same variable because they can both see and modify it. Two separate processes, by default, cannot race over a Python variable the same way, because each process has its own independent copy of everything — there's no shared state to race over unless it's explicitly, deliberately set up (via the IPC mechanisms the OS book's Chapter 8 covers: shared memory, pipes, message queues, sockets).

import multiprocessing

counter = 0

def increment():
    global counter
    for _ in range(100_000):
        counter += 1

processes = [multiprocessing.Process(target=increment) for _ in range(4)]
for p in processes:
    p.start()
for p in processes:
    p.join()

print(counter)  # Always 0 -- each process got its OWN independent copy of `counter`
                 # at fork time; none of their increments are visible to the parent at all.

This isn't a bug fix — it's a fundamentally different problem. The threaded version (Chapter 1) had a race condition because the threads shared the same counter. This process version has no race condition, but also no shared result — each process silently incremented its own private copy, and none of that work is visible anywhere else. To get a correct, shared count across processes, you'd need explicit IPC — a multiprocessing.Value (a shared-memory primitive) or a shared queue — deliberately opting back into shared state, with all its associated coordination requirements.

import multiprocessing

def increment(shared_counter, lock):
    for _ in range(100_000):
        with lock:  # explicit synchronization -- covered fully in Chapter 4
            shared_counter.value += 1

if __name__ == "__main__":
    shared_counter = multiprocessing.Value('i', 0)
    lock = multiprocessing.Lock()
    processes = [multiprocessing.Process(target=increment, args=(shared_counter, lock)) for _ in range(4)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()
    print(shared_counter.value)  # 400,000 -- correct, because the lock serialized access

The trade-off: isolation avoids some bugs, but costs communication

[SENIOR INSIGHT] This is the practical version of the OS book's isolation-versus-sharing trade-off, specifically as it applies to concurrent program design: processes give you race-condition immunity for free, by default, at the cost of needing explicit, deliberate mechanisms for any actual data sharing or coordination you DO need. Threads give you free, immediate, zero-effort data sharing, at the cost of needing explicit, deliberate synchronization for every single piece of that shared state to avoid races. Neither approach eliminates the need for careful design — they just shift where that careful design is required: processes require deliberate effort to communicate; threads require deliberate effort to isolate/synchronize.

ThreadsProcesses
MemoryShared by defaultIsolated by default
Race conditions possible?Yes, on any shared mutable state, by defaultNo, unless shared state is deliberately set up via IPC
Communication costFree (shared memory, direct access)Requires explicit IPC (OS book, Chapter 8)
Fault isolationWeak — a crash in one thread can take down the whole processStrong — a crashed process doesn't directly affect others
Best fitI/O-bound work needing to share results/state cheaplyCPU-bound work needing true parallelism (sidesteps Chapter 10's GIL), or work needing strong fault isolation

Common mistakes

  1. Choosing threads by default without considering whether process-based isolation would eliminate an entire category of bugs for a workload that doesn't actually need fine-grained data sharing.
  2. Choosing multiprocessing for CPU-bound parallelism in Python but forgetting that IPC has real overhead (serializing data between processes, per the OS book's Chapter 8) — passing large amounts of data between processes can itself become the bottleneck, undermining the parallelism gained.
  3. Assuming a multiprocessing-based design is "safe" from all concurrency bugs just because processes don't share memory by default — the moment shared state is deliberately introduced (via multiprocessing.Value, a shared queue, or a database), the exact same race-condition/synchronization discipline from the rest of this book applies again, in full.
  4. Not recognizing that a crashing thread can corrupt or crash the entire process, while a crashing process (in a well-designed multi-process system) can often be restarted independently without affecting its siblings — a real fault-isolation consideration when choosing between the two models for a production system's resilience needs.

Checklist: choosing between threads and processes for concurrent work

  1. Default to threads for I/O-bound work that benefits from cheap, direct data sharing — most web servers and I/O-heavy services fit this case.
  2. Default to processes (or an equivalent isolation mechanism) for CPU-bound work needing true parallelism in a GIL-constrained runtime (Chapter 10), or for workloads where fault isolation between units of work is a genuine, valued property.
  3. When using processes, account for IPC overhead explicitly — minimize how much data needs to cross the process boundary, since serialization/deserialization cost is real and can dominate for large payloads.
  4. Remember that opting into shared state across processes (via explicit IPC) reintroduces every synchronization concern this book covers — process isolation is not a substitute for careful concurrent design once shared state is deliberately added back in.

Discussion / interview questions

  1. "Why doesn't a multiprocessing version of a shared-counter program have the same race condition as a threaded version, by default?" Strong answer: Each process gets its own independent copy of memory (no sharing by default), so there's no shared mutable state for concurrent operations to race over — each process's increments only affect its own private copy, invisible to the others, which is a different problem (no shared result) rather than a race condition.

  2. "What's the fundamental trade-off between threads and processes for concurrent program design?" Strong answer: Threads share memory for free but require deliberate synchronization to avoid races on that shared state; processes isolate memory by default (no races by default) but require deliberate, explicit IPC to share any data or coordinate at all — the design effort shifts from "avoiding races" to "achieving communication," rather than disappearing.

  3. "When would you choose multiprocessing over threading for a CPU-bound Python workload?" Strong answer: When true parallelism across multiple cores is needed, since CPython's GIL (Chapter 10) prevents genuine parallel execution of Python bytecode across threads — multiprocessing sidesteps this by using separate processes, each with its own interpreter and GIL.

  4. "If you deliberately share state across processes using multiprocessing.Value, do you still need to worry about race conditions?" Strong answer: Yes — the moment shared, mutable state is explicitly introduced across processes, the same synchronization discipline (locks, and the rest of Chapter 4's primitives) applies exactly as it would for threads; process isolation only protects against races on state that ISN'T deliberately shared.

Chapter summary / cheat sheet

  • Race conditions require shared mutable state — threads have this by default (shared memory); processes don't (isolated memory by default), which is why a naive multi-process "shared counter" silently loses all updates rather than racing.
  • The trade-off: threads = free sharing, cost is synchronization discipline. Processes = free isolation (no races by default), cost is explicit IPC for any needed sharing/coordination.
  • Deliberately shared state across processes (via multiprocessing.Value, shared queues, etc.) reintroduces the full synchronization discipline this book covers — process isolation isn't a substitute for careful design once you opt back into sharing.
  • Choose threads for I/O-bound work needing cheap shared state; choose processes for CPU-bound work needing true parallelism (Chapter 10) or for genuine fault-isolation needs.