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

Chapter 1: Concurrency vs. Parallelism, and Why It's Hard

Why this matters

"Concurrency" and "parallelism" are used interchangeably in casual conversation, but they describe genuinely different things, and confusing them leads to real design mistakes — like assuming adding threads to a CPU-bound Python program will speed it up (Chapter 10 explains exactly why it often won't), or assuming an async, single-threaded system can't have race conditions (it very much can, just from a different cause). Getting the distinction precise is the foundation everything else in this book builds on.

The core concept

[BEGINNER] Concurrency is about structure: dealing with multiple tasks that are logically in progress at the same time, whether or not they're literally executing at the same instant. Parallelism is about execution: multiple tasks literally running at the exact same instant, on different physical CPU cores.

[SENIOR INSIGHT] A single CPU core, rapidly switching between tasks (Chapter 4 of the companion Operating System book's scheduling and context-switching mechanics), can be concurrent without being parallel — from an outside observer's perspective, multiple tasks appear to make progress "simultaneously," but at any given nanosecond, only one is actually executing. Conversely, running the exact same, fully independent computation on two separate cores with no interaction between them is parallel without needing any concurrency-aware design at all — if the two computations never touch shared state, there's no coordination problem to solve, even though they're genuinely running at the same instant.

Concurrency (structure) — one core, time-sliced:
Core 1: [Task A][Task B][Task A][Task B][Task A]...
        Both tasks make progress "at the same time" from the outside,
        but never literally execute simultaneously.

Parallelism (execution) — two cores, simultaneous:
Core 1: [Task A][Task A][Task A]...
Core 2: [Task B][Task B][Task B]...
        Both genuinely executing at the exact same instant.

The reason this distinction matters practically: concurrency is what introduces the coordination problems this entire book is about (race conditions, deadlock, the need for synchronization) — and, critically, this happens even on a single core with no parallelism at all, because the problem is about tasks interleaving unpredictably, not about literal simultaneous execution. Parallelism, on its own, introduces no coordination problem whatsoever if the parallel tasks don't share any mutable state.

A concrete demonstration: a race condition with no parallelism involved

import threading

counter = 0

def increment():
    global counter
    for _ in range(100_000):
        counter += 1  # NOT atomic -- read, add, write are three separate steps

threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(counter)  # Expected 400,000. Actual: some smaller, inconsistent number.

This program has a genuine race condition, and it reproduces this bug even under CPython's Global Interpreter Lock (Chapter 10 covers the GIL in depth), which means only one Python bytecode instruction runs at any literal instant — there is no true parallelism happening here at all in CPython. The bug exists purely because of concurrency: the four threads' counter += 1 operations can interleave in an order that loses updates, regardless of whether they're executing in true parallel or merely time-sliced on one core. This is the single clearest illustration in this book of why concurrency, not parallelism specifically, is the actual source of the hazards covered in Chapter 3.

Why concurrency is fundamentally hard

[BEGINNER] Sequential code has one, deterministic execution order — the same input always produces the same sequence of operations. Concurrent code has many possible interleavings of its tasks' individual operations, and a bug might only manifest under one specific, rare interleaving — which is exactly why concurrency bugs are notoriously difficult to reproduce, debug, and even detect via testing (a test suite that happens to always execute operations in a "lucky" order can pass reliably in CI while the exact same code fails intermittently in production, where a different interleaving eventually occurs).

Sequential: Step1 -> Step2 -> Step3 -> Step4  (exactly ONE possible order)

Concurrent (2 threads, 2 steps each): MANY possible interleavings, e.g.:
A1, A2, B1, B2   |   A1, B1, A2, B2   |   B1, A1, B2, A2   |   B1, B2, A1, A2   | ... etc.
Only SOME of these interleavings might produce a bug -- and which one occurs
on any given run depends on scheduling decisions largely outside your control.

Common mistakes

  1. Assuming "multi-threaded" automatically means "faster." Whether concurrency delivers a speedup depends entirely on whether the underlying execution is also parallel (Chapter 10's GIL discussion is the canonical counter-example) and whether the workload is I/O-bound or CPU-bound (the OS book's Chapter 9 covers exactly this distinction).
  2. Assuming single-threaded, single-core code can't have concurrency bugs. An async, single-threaded event loop (Chapter 7) still interleaves multiple logical tasks and can absolutely have race-condition-like bugs if shared state is mutated across await points without care — concurrency's hazards come from interleaving, not from literal parallel execution.
  3. Treating a concurrency bug that "usually doesn't happen" as low-priority, missing that the interleaving that triggers it might simply be rare in testing and common under real production load/timing — "rare in testing" is not the same as "rare in production."
  4. Not distinguishing whether a performance problem calls for concurrency (better structure for I/O-bound waiting) or parallelism (more literal computational throughput for CPU-bound work) — reaching for the wrong one wastes effort and can even make things worse (thread overhead with no parallelism benefit, for instance).

Checklist: reasoning correctly about concurrency and parallelism

  1. Ask "is this task I/O-bound or CPU-bound?" before choosing a concurrency strategy — I/O-bound work benefits from concurrency (structure, overlapping waits) even without parallelism; CPU-bound work needs actual parallelism (multiple cores genuinely computing simultaneously) to see a real speedup.
  2. Never assume single-threaded code is automatically free of concurrency hazards — check for shared mutable state accessed across suspension points (an await, a callback, a signal handler) even in single-threaded designs.
  3. When a concurrency bug is hard to reproduce, suspect a specific, rare interleaving rather than dismissing it as a fluke — add logging or use dedicated concurrency-testing tools (Chapter 11) rather than assuming it will "go away."
  4. Separate the question "should this be concurrent?" (a structural/design question) from "should this be parallel?" (an execution/performance question) — they're related but answered by different considerations.

Discussion / interview questions

  1. "What's the difference between concurrency and parallelism?" Strong answer: Concurrency is a structural property — dealing with multiple logically-in-progress tasks, which may or may not execute at literally the same instant; parallelism is an execution property — tasks genuinely running simultaneously on separate cores. Concurrency can exist without parallelism (time-slicing on one core) and parallelism can exist without concurrency's hazards (independent tasks sharing no state).

  2. "Can a program have a race condition even if it never runs two things in true parallel?" Strong answer: Yes — the counter += 1 example under CPython's GIL demonstrates this precisely: no true parallel execution occurs, but unpredictable interleaving of the read-modify-write sequence across threads still produces a genuine race condition and lost updates.

  3. "Why are concurrency bugs notoriously hard to reproduce and debug?" Strong answer: Concurrent code has many possible interleavings of its operations, and a bug may only manifest under one specific, relatively rare interleaving — a test run or production instance that happens to execute in a "safe" order won't reveal the bug, making it appear intermittent and hard to pin down.

  4. "If a program's performance bottleneck is CPU-bound, would adding more threads necessarily help?" Strong answer: Only if the underlying execution model actually achieves parallelism across multiple cores for that CPU-bound work — in a runtime where a global lock serializes execution (like CPython's GIL, Chapter 10), adding more threads to CPU-bound work provides no speedup and can even add overhead without benefit.

Chapter summary / cheat sheet

  • Concurrency = structural: multiple logically-in-progress tasks, possibly interleaved rather than simultaneous. Parallelism = execution: tasks genuinely running at the same instant on separate cores.
  • Concurrency's hazards (race conditions, deadlock, etc.) come from unpredictable interleaving — they can occur with zero true parallelism, as CPython's GIL-protected race condition example demonstrates.
  • Concurrent code has many possible operation interleavings; a bug triggered by only one rare interleaving is exactly why concurrency bugs are hard to reproduce and can pass reliably in testing while failing intermittently in production.
  • Whether concurrency (or parallelism) actually helps performance depends on whether the workload is I/O-bound (concurrency helps, even without parallelism) or CPU-bound (needs genuine parallelism to see a speedup).