CMD Guide
HomeConcurrencyConcurrency Foundations

What is Multithreading and Concurrency

The mechanism in one sentence

An OS scheduler hands each runnable thread a brief time slice of a CPU core, then preempts it and runs another, so on one core threads take turns (interleaving) while on several cores they literally run at the same instant (parallelism) — concurrency is the program structure that lets either happen, parallelism is the hardware actually doing it.

That single distinction is the whole lesson. The old framing “multithreading helps achieve parallelism” is the trap: you can have concurrency on a single-core machine with zero parallelism, and you can have parallelism (e.g. SIMD vector instructions) with no threads at all. Keep them separate:

Rob Pike's line is the one to remember: “Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.”

A traced example: 3 tasks, 1 core vs 2 cores

Say a request handler must do three independent CPU jobs, each needing 30 ms of CPU work: A (resize image), B (compute checksum), C (render thumbnail). We model the scheduler with a 10 ms time slice (real Linux CFS slices are dynamic, ~1–6 ms; 10 ms keeps the trace readable).

Case 1 — concurrent on 1 core (no parallelism)

The scheduler round-robins one slice at a time. Total work is 90 ms and one core does 1 ms of work per ms, so wall-clock = 90 ms. Concurrency bought us interleaved progress and responsiveness — not speed.

Wall clock (ms)Core 0 runsA leftB leftC left
0–10A203030
10–20B202030
20–30C202020
30–60A,B,C (one more round each)101010
60–90A,B,C (final round each)000

Case 2 — parallel on 2 cores

Now two cores run truly simultaneously. A and B start together; whichever core frees first picks up C. Total CPU work is still 90 ms, but two cores deliver 2 ms of work per wall-clock ms, so the floor is 90/2 = 45 ms.

Wall clock (ms)Core 0Core 1
0–30A (runs to completion)B (runs to completion)
30–60C (runs to completion)idle

That naive split finishes in 60 ms because C couldn't start until a core freed at 30 ms — the third task serializes behind the first two. Speedup = 90/60 = 1.5×, not 2×, even with 2 cores. This is Amdahl's law in miniature: with 3 equal tasks on 2 cores the critical path is 2 sequential 30 ms chunks. The lesson: adding cores does not linearly add speed — the dependency/granularity structure caps it.

diagram
diagram

The same logic in code: Java vs Go

Both run tasks A, B, C concurrently and collect their results. Watch what the runtime schedules onto cores.

Java — platform threads via an executor + futures

import java.util.concurrent.*;
import java.util.List;

public class Tasks {
    // Each task: simulate 30 ms of CPU work, return a labelled result.
    static String work(String name) {
        long deadline = System.nanoTime() + 30_000_000L; // 30 ms
        long spins = 0;
        while (System.nanoTime() < deadline) spins++;     // busy-work
        return name + " done (" + spins + " spins)";
    }

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(2); // cap at 2 cores
        List<Future<String>> futures = List.of(
            pool.submit(() -> work("A")),
            pool.submit(() -> work("B")),
            pool.submit(() -> work("C")));
        for (Future<String> f : futures) System.out.println(f.get()); // join + collect
        pool.shutdown();
    }
}

A fixedThreadPool(2) means at most two of A/B/C run at once; the third waits — exactly the 60 ms critical path from the trace. Each Java thread here is a platform thread, a 1:1 wrapper over an OS thread (~1 MB stack), which is why you pool them rather than spawning thousands. (Java 21+ adds virtual threads, closer to Go's model — that's a later lesson.)

Go — goroutines + a channel

package main

import (
    "fmt"
    "runtime"
    "time"
)

func work(name string, out chan<- string) {
    deadline := time.Now().Add(30 * time.Millisecond)
    spins := 0
    for time.Now().Before(deadline) { // busy-work, 30 ms
        spins++
    }
    out <- fmt.Sprintf("%s done (%d spins)", name, spins)
}

func main() {
    runtime.GOMAXPROCS(2) // cap parallelism at 2 cores
    out := make(chan string)
    for _, name := range []string{"A", "B", "C"} {
        go work(name, out) // spawn a goroutine
    }
    for i := 0; i < 3; i++ {
        fmt.Println(<-out) // receive blocks until a result arrives
    }
}

Each go work(...) launches a goroutine — a user-space task with a tiny ~2 KB growable stack, multiplexed M:N onto a small pool of OS threads by the Go runtime. The channel out is how goroutines hand results back: the receiver blocks until a value is sent, which both synchronizes and transfers the result in one operation.

Where the two runtimes differ

 Java (platform threads)Go (goroutines)
Mapping to OS threads1:1 — one OS thread per Java threadM:N — many goroutines per OS thread, scheduled by Go runtime
Cost to create~1 MB stack; pool them~2 KB growable stack; spawn millions freely
CoordinationShared memory + locks; wait()/notify(), Future.get()Channels (“share by communicating”); also has sync.Mutex
Parallelism knobPool size / available coresGOMAXPROCS

Pitfalls

Takeaways


Sources: Rob Pike, “Concurrency Is Not Parallelism” (Go talk, 2012) for the structure-vs-execution distinction; Brian Goetz et al., Java Concurrency in Practice (Ch. 1) for the thread/process model and pool sizing; The Go Programming Language (Donovan & Kernighan, Ch. 8–9) for goroutines and channels; Gene Amdahl's 1967 paper for the speedup ceiling; Linux CFS documentation for time-slice behaviour. Re-authored and deepened for this guide — replaced the pure office/cooking analogy and the “multithreading achieves parallelism” conflation with an explicit mechanism, a traced 1-core-vs-2-core example, a timeline diagram, and side-by-side Java/Go code.

Work–span model (Cilk) for the 1.5× miniature

The 90 ms / 60 ms / 1.5× story connects to the classic work–span model used in Cilk and modern DAG-parallel runtimes — but with a subtlety worth getting exactly right, because the 1.5× here is not the work–span lower bound talking.

Mechanism: work W, span T, processors P

Dependency DAG for this miniature: three nodes A,B,C with no edges between them (fully independent), plus a virtual source→{A,B,C}→sink join. Nothing forces serialization at the DAG level; the 1.5× comes purely from too few cores for indivisible tasks.

QuantityValueMeaning
W = T1 (work)90 msTotal instruction-time; one-core wall clock
T (span)30 msLongest dependent chain; independent tasks ⇒ one 30 ms task. Machine-independent.
W/T3Average available parallelism = speedup ceiling with ∞ cores
T2 (actual, 2 cores)60 ms⌈3/2⌉ rounds × 30 ms — exceeds the 45 ms greedy floor because tasks are indivisible
Speedup on 2 cores1.5×90/60; capped by task granularity, not by data dependencies

With infinite cores the same DAG finishes in T = 30 ms (one layer of three parallel tasks) → speedup ≤ T1/T = 90/30 = 3×. So the 1.5× on two cores is a finite-cores-plus-indivisible-tasks effect, not a data dependency. Staff follow-up: if C depended on A's output, an edge A→C would raise the span to T ≥ 60 ms even with infinite cores — that would be a genuine critical-path limit rather than a granularity one.

When not to use the work–span bound alone

Failure / ops fingerprint

Dashboard shows 16 cores, p50 latency only ~1.5× better than single-thread for a "fully parallel" job → measure ready-queue depth and critical path (tracing spans), not just core count. Often the join barrier or a single serial reduce is the hidden T.

Drill ladder

🤖 Don't fully get this? Learn it with Claude

Stuck on What is Multithreading and Concurrency? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **What is Multithreading and Concurrency** (Concurrency) and want to truly understand it. Explain What is Multithreading and Concurrency from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **What is Multithreading and Concurrency** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **What is Multithreading and Concurrency** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **What is Multithreading and Concurrency** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes