Advanced Concurrency & Asynchrony

the M:N versus 1:1 threading model

Suppose you have a hundred errands and ten cars. The 1:1 plan: buy one hundred cars, one per errand — simple, but cars are expensive and your driveway overflows. The M:N plan: keep your ten cars and a clever dispatcher that rotates the hundred errands through them, so each car serves many errands over the day. In threading, the 'errands' are your logical tasks (coroutines, goroutines, fibers) and the 'cars' are OS threads that the kernel actually schedules onto cores. The ratio names who maps to what.

In the 1:1 model, every user thread is backed by one OS (kernel) thread — this is what std::thread, pthreads, and the JVM's platform threads give you. The kernel schedules each one directly; they are real, preemptible, and can run truly in parallel on separate cores, but each carries a full OS thread's cost (a stack of ~1 MiB, kernel bookkeeping, and a kernel-mediated context switch). In the M:N model, a language runtime multiplexes M lightweight user-level threads onto N OS threads (typically N is around the core count). These M tasks — Go's goroutines, Erlang processes, Java's new virtual threads — are cheap (a goroutine starts at ~2 KiB), and switching between them is a fast user-space operation, no kernel call. The runtime's scheduler (often work-stealing) parks a task at its yield points and runs another on the same OS thread, so a handful of OS threads serve hundreds of thousands of tasks. This is the same machinery behind green threads and stackful coroutines.

Why the distinction matters: 1:1 is simple and the kernel handles blocking and preemption for you, but you cannot afford a million of them. M:N gives massive concurrency cheaply — a million goroutines is fine — which is ideal for servers juggling huge numbers of mostly-waiting connections. The classic M:N pitfall, and an honest one: when a user task makes a blocking system call, it blocks the underlying OS thread, and naively that would freeze all the other tasks multiplexed onto it; production M:N runtimes solve this by detecting the block and spinning up or handing off another OS thread (Go does this automatically). So M:N is not free magic — it needs a sophisticated runtime, and a poorly handled blocking call still wedges a real thread.

1:1: std::thread::spawn(f) — one OS thread per task, ~1 MiB stack each. M:N: Go's 'go f()' — millions of ~2 KiB goroutines multiplexed onto GOMAXPROCS OS threads by a work-stealing runtime scheduler.

1:1 maps each task to a kernel thread; M:N multiplexes many cheap user tasks onto a few kernel threads.

M:N is not strictly better: it needs a smart runtime to keep a blocking syscall from wedging an OS thread (and stalling co-located tasks). 1:1 is heavier but simpler, and the kernel handles preemption and blocking for free. Most languages pick one; some (Java) now offer both.

Also called
green threadsuser-level threadshybrid threading綠色執行緒使用者層級執行緒