Not sure where I am going with this garbage collection rabbit hole

argentum471 pts0 comments

A Quick rundown on concurrency and gc - iko's logs

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<br>same problem (run many logical tasks on few OS threads) with three different<br>answers to one question: who controls the switch between tasks, and what<br>does that controller need to know to do it safely?

The answer to that question determines everything downstream: whether the<br>model is cooperative or preemptive, whether GC pauses one thread or the whole<br>process, and whether a crash is contained or catastrophic.

This document derives each model from its constraints rather than describing<br>it as a list of features. Each section ends with a checkpoint question you<br>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<br>kernel’s abstraction for time-slicing a core across many instruction<br>streams, but they are expensive:

~8MB stack per thread (Linux default)

A context switch saves/restores the full register file and disturbs the<br>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 :<br>M logical tasks multiplexed onto N OS threads (typically N ≈ number of<br>cores). The three systems below are three different M:N schedulers, and<br>they differ because they made different decisions about who owns the<br>switching logic.

Checkpoint: before continuing, state in one sentence why an OS thread is<br>too expensive to use one-per-logical-task at scale.

Part 1 — Go

Sources read directly for this section (not paraphrased from memory):<br>src/runtime/preempt.go, src/runtime/signal_unix.go, src/runtime/proc.go,<br>src/runtime/mgc.go — golang/go, master branch, fetched from<br>raw.githubusercontent.com.

1.1 Key concept: this is CSP, not fork-join

Go’s concurrency model is explicitly an implementation of Hoare’s<br>Communicating Sequential Processes (CSP, 1978) — independent sequential<br>processes that interact only through message passing over channels, not<br>shared mutable state accessed via locks. This is a design lineage, stated<br>directly in Go’s own materials: “Don’t communicate by sharing memory;<br>share memory by communicating.”

Where CSP itself came from. Tony Hoare published “Communicating<br>Sequential Processes” in Communications of the ACM, 1978. The problem he<br>was working on wasn’t concurrency in the modern web-service sense — it was<br>correctness of concurrent programs at a time when shared-variable<br>concurrency (semaphores, monitors) was the dominant model and was proving<br>extremely hard to reason about formally: with shared mutable state, the<br>number of possible interleavings of two processes explodes, and proving a<br>program correct meant proving it correct under all of them.

Hoare’s move was to make the only interaction between processes an<br>explicit, synchronous, named event — a process names who it’s sending<br>to/receiving from, and the send/receive pair is the entire synchronization<br>primitive, with no separate lock needed. This has a real mathematical<br>payoff: because processes share nothing, you can reason about each process<br>in isolation and about the communication events between them as a<br>separate, much smaller problem — closer to algebra than to exhaustive<br>case analysis of shared-memory interleavings. CSP was formalized further<br>into a full process algebra in Hoare’s own later work and independently<br>alongside Robin Milner’s CCS (Calculus of Communicating Systems, also late<br>1970s) — the two are usually cited together as the origin of process<br>algebras generally.

Go’s designers (Rob Pike, in particular, who had earlier worked on<br>Newsqueak and Alef — direct experimental predecessors that already used<br>CSP-style channels) took the communication primitive from CSP —<br>synchronous, named-channel message passing — without adopting Hoare’s full<br>formal process algebra or his original synchronous-only restriction (Go’s<br>buffered channels allow asynchronous sends up to the buffer size, which<br>Hoare’s original calculus didn’t have). So “Go implements CSP” is accurate<br>at the level of the core idea — channels as the unit of synchronization,<br>not locks — and imprecise if taken to mean Go implements the full formal<br>calculus.

This matters because it’s easy to conflate with two other models that are<br>not what Go does:

Fork-join (Java’s ForkJoinPool, Cilk, OpenMP): a task explicitly<br>splits into subtasks, waits for all of them, then joins. The parallelism<br>is structured around a single computation’s divide-and-conquer shape.<br>Go has nothing built into the language for this — you’d hand-roll it<br>with a sync.WaitGroup. Goroutines are not spawned with an implicit<br>join; go f() returns immediately and nothing waits for it unless you<br>add that synchronization yourself.

Shared-memory threading with...

process from hoare processes concurrency three

Related Articles