Pangram verdict · v3.3
We believe that this text is a mix of AI and human-written content.
AI likelihood · overall
AIArticle text · 936 words · 2 segments analyzed
Related: 001_usage_of_signals_in_language_runtime.md, 006_other_reading_materials.md, 007_concurrency_comparison.md Abstract Three languages — Go, Kotlin, and Erlang/Elixir (running on BEAM) — solve the same problem (run many logical tasks on few OS threads) with three different answers to one question: who controls the switch between tasks, and what does that controller need to know to do it safely? The answer to that question determines everything downstream: whether the model is cooperative or preemptive, whether GC pauses one thread or the whole process, and whether a crash is contained or catastrophic. This document derives each model from its constraints rather than describing it as a list of features. Each section ends with a checkpoint question you should be able to answer before moving to the next section. Background: the problem all three are solving A CPU core runs one instruction stream at a time. OS threads are the kernel’s abstraction for time-slicing a core across many instruction streams, but they are expensive: ~8MB stack per thread (Linux default) A context switch saves/restores the full register file and disturbs the cache and TLB 10,000 OS threads means gigabytes of stack space before any work is done So every runtime that wants cheap concurrency builds an M:N scheduler: M logical tasks multiplexed onto N OS threads (typically N ≈ number of cores). The three systems below are three different M:N schedulers, and they differ because they made different decisions about who owns the switching logic. Checkpoint: before continuing, state in one sentence why an OS thread is too expensive to use one-per-logical-task at scale. Part 1 — Go Sources read directly for this section (not paraphrased from memory): src/runtime/preempt.go, src/runtime/signal_unix.go, src/runtime/proc.go, src/runtime/mgc.go — golang/go, master branch, fetched from raw.githubusercontent.com. 1.1 Key concept: this is CSP, not fork-join Go’s concurrency model is explicitly an implementation of Hoare’s Communicating Sequential Processes (CSP, 1978) — independent sequential processes that interact only through message passing over channels, not shared mutable state accessed via locks. This is a design lineage, stated directly in Go’s own materials: “Don’t communicate by sharing memory; share memory by communicating.” Where CSP itself came from. Tony Hoare published “Communicating Sequential Processes” in Communications of the ACM, 1978. The problem he was working on wasn’t concurrency in the modern web-service sense — it was correctness of concurrent programs at a time when shared-variable concurrency (semaphores, monitors) was the dominant model and was proving extremely hard to reason about formally: with shared mutable state, the number of possible interleavings of two processes explodes, and proving a program correct meant proving it correct under all of them. Hoare’s move was to make the only interaction between processes an explicit, synchronous, named event — a process names who it’s sending to/receiving from, and the send/receive pair is the entire synchronization primitive, with no separate lock needed. This has a real mathematical payoff: because processes share nothing, you can reason about each process in isolation and about the communication events between them as a separate, much smaller problem — closer to algebra than to exhaustive case analysis of shared-memory interleavings. CSP was formalized further into a full process algebra in Hoare’s own later work and independently alongside Robin Milner’s CCS (Calculus of Communicating Systems, also late 1970s) — the two are usually cited together as the origin of process algebras generally. Go’s designers (Rob Pike, in particular, who had earlier worked on Newsqueak and Alef — direct experimental predecessors that already used CSP-style channels) took the communication primitive from CSP — synchronous, named-channel message passing — without adopting Hoare’s full formal process algebra or his original synchronous-only restriction (Go’s buffered channels allow asynchronous sends up to the buffer size, which Hoare’s original calculus didn’t have). So “Go implements CSP” is accurate at the level of the core idea — channels as the unit of synchronization, not locks — and imprecise if taken to mean Go implements the full formal calculus. This matters because it’s easy to conflate with two other models that are not what Go does: Fork-join (Java’s ForkJoinPool, Cilk, OpenMP): a task explicitly splits into subtasks, waits for all of them, then joins. The parallelism is structured around a single computation’s divide-and-conquer shape. Go has nothing built into the language for this — you’d hand-roll it with a sync.WaitGroup. Goroutines are not spawned with an implicit join; go f() returns immediately and nothing waits for it unless you add that synchronization yourself. Shared-memory threading with locks (raw pthreads, Java synchronized): the default coordination primitive is a shared address space guarded by mutual exclusion. Go supports this too (sync.Mutex exists and is used heavily inside the runtime itself), but it’s not the idiomatic surface the language pushes you toward. Channels (chan) are the CSP primitive: a goroutine sends a value into a channel, another receives it, and the transfer itself is the synchronization point — no separate lock is needed for that handoff. Checkpoint: if goroutines don’t implicitly join, what actually guarantees a go f() call’s side effects are visible before main() returns? (Answer: nothing, by default — this is why programs that don’t explicitly wait via a channel or WaitGroup can exit before spawned goroutines finish; it’s a common bug source, not a language guarantee.)
1.2 The GMP scheduler From proc.go’s own top-of-file doc comment (line ~24 onward): The scheduler’s job is to distribute ready-to-run goroutines over worker threads. G - goroutine. M - worker thread, or machine. P - processor, a resource that is required to execute Go code. M must have an associated P to execute Go code, however it can be blocked or in a syscall w/o an associated P. Design doc at https://golang.org/s/go11sched.