Concurrency Roadmap/Concurrency Foundations

Concurrency vs. Parallelism

Two words interviewers use almost interchangeably that mean genuinely different things: one is about structure, the other about simultaneous execution — and only one of them requires extra hardware.

!!1/5Theory: 25m
Language-specific mechanics: Concurrency Language Manual — Concurrency Models & Runtime Fundamentals

Two words, two different questions

"Concurrency" and "parallelism" get used interchangeably in casual conversation, and that habit will cost you in a senior-level interview. They answer two different questions:

  • Concurrency asks: is my program structured to deal with more than one thing in progress at once? It's a property of design — how you decompose a problem into independently-progressing pieces.
  • Parallelism asks: are two things physically executing at the exact same instant? It's a property of execution — it requires actual hardware capable of doing more than one thing simultaneously (multiple cores, multiple machines, a GPU's many ALUs).

The cleanest formulation, borrowed from Rob Pike's well-known talk on the subject (linked below): concurrency is about structure, parallelism is about execution. You can have one without the other, and mixing them up is the single most common conceptual error candidates make when discussing multithreaded systems.

Concurrency without parallelism

A single-core machine running a preemptively-scheduled OS can absolutely run multiple threads "concurrently" — the scheduler rapidly switches the one physical core between them (see the next subtopic for exactly how), giving the illusion that they progress together. At any given nanosecond, though, only one instruction stream is actually executing. This is concurrency with zero parallelism, and it's a completely legitimate, common, and useful configuration — it's precisely how a single-threaded event loop (Node.js, a GUI's UI thread, Python's asyncio) provides responsiveness for many in-flight I/O operations without ever running two pieces of your code at the same literal instant.

Parallelism without (interesting) concurrency

The inverse case exists too, though it's less commonly discussed: pure data parallelism, where the exact same operation runs simultaneously across many data elements with no interesting coordination between them (think: adding two arrays element-by-element on a GPU, or a SIMD instruction). There's genuine simultaneous execution here, but the "structure" is trivial — there's no independent decision-making or communication between the parallel units, which is what concurrency is really concerned with.

The relationship, precisely

Parallelism is best understood as a possible property of concurrent structure, not a separate axis:

Concurrent structureNo concurrent structure
Parallel hardware available & exploitedGenuinely useful: independent tasks actually run simultaneouslyN/A — nothing to parallelize
Parallel hardware unavailable, or not exploitedTasks interleave on shared resources (time-sliced)A plain sequential program

This is why Pike's framing lands so well: concurrency is what makes a program capable of being run in parallel; parallelism is a runtime decision about whether it actually is, made by the scheduler and the number of available cores, not by the program's source code. The same concurrently-structured program — say, a web server handling each request as an independent task — runs with zero parallelism on a single-core VM and with real parallelism on a 32-core box, without a single line of application code changing.

A worked example to make this concrete

Imagine four independent tasks, each of which does some CPU work:

tasks = [A, B, C, D]
  • Sequential (no concurrency, no parallelism): run A, then B, then C, then D, one after another. Total wall-clock time ≈ sum of all four.
  • Concurrent, not parallel (one core, time-sliced): the scheduler interleaves A/B/C/D in small slices. All four make progress "together," but total wall-clock time is still ≈ sum of all four (plus context-switch overhead) — you've improved responsiveness and structure, not throughput.
  • Concurrent and parallel (four cores): A, B, C, and D genuinely run at the same instant. Total wall-clock time ≈ the time for the single slowest task (ignoring overhead) — this is the only one of the three that actually reduces total time for CPU-bound work.

This is exactly why "just add threads" doesn't automatically speed up a CPU-bound workload on a single-core machine (or in a runtime with a global lock serializing execution) — you've added concurrency, not parallelism, and only parallelism reduces wall-clock time for CPU-bound work.

Why interviewers probe this distinction

This shows up constantly disguised as a design question: "how would you speed up processing 10,000 independent records?" A candidate who reaches for "just multithread it" without asking "is this I/O-bound or CPU-bound, and how many cores do I actually have?" is signaling that they've memorized "threads = faster" without understanding when that's true. The strong answer distinguishes: concurrency (structuring the work as independent units, useful for I/O-bound work even on one core, since threads can overlap waiting) from parallelism (needing that work to actually run simultaneously to reduce wall-clock time, which only helps CPU-bound work and only up to the number of available cores).

Pitfalls and interview gotchas

  • "More threads" is not automatically "faster." On a CPU-bound workload with more threads than cores, you're adding context-switch overhead without adding real throughput — sometimes making things slower.
  • Forgetting that I/O-bound work benefits from concurrency alone. A thread blocked waiting on a network response isn't consuming a core; overlapping many such waits is a legitimate concurrency win with zero parallelism required.
  • Assuming a "concurrent" language feature implies parallel execution. Python's asyncio and JavaScript's async/await are concurrency mechanisms that run on a single thread by default — genuinely useful, but not parallelism, and not a fix for CPU-bound work (see Concurrency Foundations' first subtopic on the GIL).
  • Treating this as purely academic. "Is this concurrent, parallel, both, or neither?" is a legitimate, answerable question about any system design, and being able to answer it crisply is a strong signal in an interview.

Where this leads

With concurrency and parallelism cleanly separated, Scheduling & Context Switching (next) explains the actual mechanism that produces concurrency on shared hardware — what the OS scheduler decides, how it enforces preemption, and what it costs every time it switches the CPU from one unit of execution to another.

Reference implementations in:

Same task list, concurrent-only vs. actually parallel execution

In each language, notice that the code expressing "run these independent tasks" barely changes — what changes is the execution resource behind it.

// Same `go compute()`; only GOMAXPROCS changes execution reality. runtime.GOMAXPROCS(1) // concurrent: many goroutines, one OS thread for i := 0; i < 4; i++ { go compute() } runtime.GOMAXPROCS(4) // actually parallel across up to 4 cores for i := 0; i < 4; i++ { go compute() }

GOMAXPROCS caps how many goroutines run truly in parallel (default: runtime.NumCPU() since Go 1.5). With it set to 1, goroutines still interleave (concurrency) but never overlap on CPU. Bump it to the core count and the exact same go statements become genuinely parallel — there is no GIL.

Further Resources (Optional)