Threadsconcurrencyparallelismtime slicinginterleavingcores

Concurrency versus Parallelism

Concurrency is having several tasks in progress at once, achieved on a single core by interleaving them in time slices; parallelism is executing several at the same instant, which requires several cores — a single core is always concurrent and never parallel.

ConceptualLinux
▶ InteractiveInterview question
Progress

The problem

A 2005 laptop with one core ran a browser, a music player and a compiler "at the same time". A modern phone with eight cores does the same thing. Were they doing the same thing? And when you add a second thread to a program and it gets no faster, which of the two words was wrong?

One core, many tasks

Conceptual

A single core executes one instruction stream. To run three programs it runs A for a few milliseconds, saves A’s registers, loads B’s, runs B, and so on — time slicing, driven by a hardware timer interrupt that hands control back to the scheduler roughly every 1–10 ms (Linux’s default scheduler targets a few milliseconds of latency, so the slice shrinks as more tasks compete). At human timescales the three programs appear simultaneous; at the hardware timescale exactly one is ever running.

That is concurrency: the *structure* of a system in which several tasks are in progress — each has started, none has finished — and the system makes progress on all of them by interleaving. It is a property of the program and the scheduler, not of the hardware. A single-core machine is fully concurrent. An event loop on one thread is fully concurrent. Neither runs two instructions at the same instant.

Many cores, same instant

Parallelism is *execution*: two or more instruction streams advancing in the same clock cycle on different cores (or different hardware threads of a core, or SIMD lanes, or GPU units). It is a property of the hardware plus a program structured so that independent work can be handed to different cores. Parallelism without concurrency is possible in the narrow sense — a single vectorised loop — but in practice you get parallelism by first making the program concurrent (splitting it into tasks) and then having enough cores to run the tasks at once.

The scheduler is what turns one into the other. With eight runnable threads and eight cores, it places one on each and they run in parallel. With eight threads on one core, it time-slices them and they run concurrently. The program is identical; only the parallelism changes. That is why "add threads" does not mean "get faster": threads add concurrency, and speed-up needs spare cores *and* work that does not wait on the other threads.

  • Concurrency: dealing with many things at once (a design property). Parallelism: doing many things at once (an execution property) — Rob Pike’s formulation.
  • Concurrent, not parallel: one core with time slicing; an event loop; CPython threads under the GIL.
  • Parallel: eight threads on eight cores; eight processes; a GPU kernel.

Why the distinction changes decisions

If the work is waiting — on sockets, disks, users — concurrency alone gives all the benefit, because the CPU was idle during the waits and interleaving fills the idle time. A single-core server can hold 10,000 idle connections; a second core would not help. If the work is computation, concurrency alone gives nothing: interleaving two CPU-bound tasks on one core makes both finish at the same time, later than running them one after the other would have finished the first. Only parallelism helps, and only up to the core count.

Even with parallel hardware the speed-up is bounded by the part that cannot be split. Amdahl’s law: if 10% of the work is inherently serial, infinite cores give at most 10×; on 8 cores the ceiling is about 4.7×. Coordination adds more: the lock protecting shared state serialises the threads that contend on it, and the cache-coherence traffic of two cores writing the same line ("false sharing") can make a parallel version *slower* than the serial one. See Atomic Operations and Mutexes for the mechanisms and their costs.

Seeing it

Linux

Pin a program to one core with taskset -c 0 ./program (Linux) and compare its wall-clock against the unpinned run. A CPU-bound multi-threaded program slows down in proportion to the cores it lost; an I/O-bound one barely changes. time prints user + sys CPU time versus real wall-clock: a parallel program has CPU time greater than wall-clock (eight cores busy for 1 s is 8 s of CPU in 1 s real); a concurrent-but-not-parallel one has CPU time at most equal to wall-clock; an I/O-bound one has CPU time far below wall-clock.

Same program, one core vs eight (illustrative numbers)
$ time ./compress big.bin            # 8 threads, 8 cores
real 0m1.9s   user 0m13.8s   sys 0m0.4s      ← ~7.5 cores busy: parallel

$ time taskset -c 0 ./compress big.bin
real 0m14.1s  user 0m13.7s   sys 0m0.3s      ← same CPU work, time-sliced: concurrent only

$ time ./fetch-urls urls.txt             # 200 async requests
real 0m0.8s   user 0m0.1s    sys 0m0.1s      ← waiting dominates: concurrency is all it needs

Key points

  • Concurrency: several tasks in progress, interleaved; a property of program structure and the scheduler. Parallelism: several tasks executing at the same instant; a property of hardware.
  • A single core is concurrent via time slicing (timer interrupt → scheduler → context switch) and never parallel.
  • Threads add concurrency; speed-up requires spare cores and independent work.
  • I/O-bound work needs only concurrency; CPU-bound work needs parallelism, bounded by the core count and by Amdahl’s law.
  • user + sys versus real in time tells you which one you have.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why time-slice at all instead of running each program to completion?

Because most programs wait, and because interactive latency matters: without preemption one CPU-bound program would freeze the keyboard for as long as it ran.

Why does adding threads to a CPU-bound program on one core make it slower?

Every context switch costs microseconds and cold caches, and there is no idle time to fill; the switches are pure overhead.

Why does the distinction matter for language choice?

Runtimes differ in which they provide: an event loop gives concurrency only; native threads give both if the runtime lets them run simultaneously; CPython’s default build gives threads that are concurrent but not parallel for Python bytecode.

Concurrency vs parallelism

Concurrency vs parallelism
Concurrency: tasks make progress in overlapping time. Parallelism: tasks execute at the same instant. One needs a scheduler, the other needs cores.
Tasks
Cores
Workload
task 0
task 1
task 2
0180 ms
CPUI/O waitready (queued)
Wall-clock
180 ms
Serial (one after another)
180 ms
Mode
interleaved → concurrency only
One core, CPU-bound: no speed-up. The scheduler time-slices, so all tasks progress together — but total CPU time is fixed and wall-clock equals the serial sum (plus switch overhead not shown).
1/18 · t = 0 µsEducational model

How it fails

What the failure looks like from inside real software.

  • A team adds threads to a CPU-bound service on a 2-vCPU container and throughput drops: oversubscription, not parallelism.
  • An async crawler is "not using all cores" — correct and irrelevant, its bottleneck is the network; the only cost is the confusion.
  • A parallel counter with a shared lock runs slower on 16 threads than on 1: the lock serialises everything and the cache line ping-pongs between cores.
  • Amdahl in practice: a 12-stage pipeline parallelises 11 stages and the 12th, single-threaded, now dominates the wall-clock.