The idea: in progress at once vs executing at once
Concurrency means several tasks are in progress during the same period of time. They may take turns on one core. Parallelism means several tasks are executing in the same instant, on different hardware (cores, CPUs, machines, vector lanes).
Rob Pike put it this way: concurrency is about dealing with lots of things at once; parallelism is about doing lots of things at once. Concurrency is a way to structure a program. Parallelism is a way to run it.
The page runs the same threads under four modes. Look at the running at once row of the timeline. If it never goes above 1 while several threads are alive, the run is concurrent but not parallel. When it is 2 or more (orange), threads run in parallel.
| not parallel | parallel | |
|---|---|---|
| not concurrent | Serial: one task after another on one core | One task split into chunks that run on several cores at once (fork–join, SIMD) |
| concurrent | Many threads time-sliced on one core; one Node.js event loop; Python threads under the GIL | A thread pool on a multicore: more threads than cores, time-sliced and spread over cores |
Time slicing on one core
The scheduler keeps a ready queue. A core takes the thread at the head and runs it for at most one quantum. When the quantum runs out and someone else is waiting, the thread is preempted and goes to the back of the queue. Switching threads is a context switch: the core saves one thread's registers and loads another's. That costs time (Demo: context switches cost time: 24 ticks of work take 35 ticks when each switch costs 1).
On CPU-bound work, time slicing on one core does not finish anything sooner. Every thread starts early (average response time 9 → 3 ticks), but every thread also finishes later (average turnaround 15 → 21). The total stays 24 ticks (Demo: CPU-bound — serial vs concurrent on 1 core). See CPU Scheduling for other scheduling policies and Linux Context Switch for what a switch does inside the kernel.
Why concurrency helps I/O-bound work
A thread waiting for the network or the disk does not need a core. In serial code a blocking call keeps the core, so the core does nothing: the I/O-bound workload takes 32 ticks with the CPU busy 50 % of the time. With concurrency the waiting thread gives the core away and another thread runs. The same workload takes 16 ticks on the same single core, and the CPU is busy 100 % of the time (Demo: I/O-bound — concurrency wins on 1 core).
This is why one event loop can serve thousands of connections: it is concurrent, not parallel. See The Node.js Event Loop and epoll.
Why only parallelism helps CPU-bound work
If every thread wants the CPU all the time, there is nothing to overlap. The only way to finish sooner is more hardware running at the same time: 24 ticks on 1 core, 12 on 2, 6 on 4 (Demo: CPU-bound on 1, 2 and 4 cores). With more threads than cores, the two combine: 4 threads time-sliced over 2 cores is concurrent and parallel (Demo: concurrent + parallel).
| workload | serial | concurrent, 1 core | parallel, 4 cores |
|---|---|---|---|
| CPU-bound (4 × cpu 6) | 24 | 24 | 6 |
| I/O-bound (4 × cpu 2, io 4, cpu 2) | 32 | 16 | 8 |
Amdahl's law
Most programs have a part that cannot be split. In the fork–join workload, main does 2 ticks of setup, forks 4 workers of 3 ticks each, joins them, and does 2 more ticks. The serial part is 4 of 16 ticks (s = 0.25). The run takes 16 ticks on 1 core, 10 on 2 and 7 on 4: a speedup of only 2.29 on 4 cores (Demo: fork–join and Amdahl's law).
Amdahl's law: speedup = 1 / (s + (1 − s) / N). With s = 0.25 no number of cores can give more than 4×.
Race conditions need only concurrency
counter++ is three machine instructions: LOAD r ← counter, ADD r ← r + 1, STORE counter ← r. On one core, if T1 is preempted after its LOAD, T2 loads the same old value. Both store 1, and one increment is lost (Demo: race on ONE core). On two cores both threads can LOAD in the same tick, with the same result (Demo: race on two cores). The bug is in the interleaving. It does not need parallel hardware.
With a quantum of 4 ticks the same code happens to give 2. Races depend on timing, which is why they are hard to reproduce. A mutex makes LOAD–ADD–STORE a critical section: T2's LOCK fails, T2 sleeps, and T1's UNLOCK hands the mutex to T2 (Demo: a mutex fixes both). See Bounded Buffer and Dining Philosophers for more synchronization.
Makespan, turnaround and response time
Makespan is when the last thread finishes. Turnaround is when each thread finishes. Response time is when each thread first gets a core. CPU busy is the share of core-ticks spent running a thread. Concurrency often trades turnaround for response time. Parallelism raises throughput when there is work to spread. See Latency vs Throughput.
See also From a Java Thread to a CPU Core: how a Java thread becomes a kernel task, how the kernel spreads tasks over logical CPUs, and why two hardware threads on one core are not two cores.
What the page leaves out
Real kernels keep one run queue per core and move threads between them for load balancing. The page uses one global queue. The page also leaves out hyper-threading (two hardware threads sharing one core), cache effects and false sharing, NUMA, memory ordering (a real race can also come from store buffers and reordering), GPUs and SIMD details, and Gustafson's law (a bigger problem on more cores).