dlang-supplemental

Blog

Isolating heaps as threads isolate data

2 August 2026 · rehash of the docs design article

D already treats ordinary variables as thread-local. std.concurrency then pushes you toward mailboxes: copy, immutable, maybe a move. That is an actor story, told with TLS instead of a second language.

The collector never got the memo. There is still one process-wide heap, and when any thread collects, every registered thread stops—including the @nogc audio callback you promised would never allocate.

Thread A and Thread B isolate TLS data and talk through a mailbox, but both still allocate into one process-wide GC heap
Private data. Public junk drawer.

Detach the realtime thread if you want a hard guarantee. After thread_detachThis() the collector will not scan it— and that thread must never touch GC memory again. You bought a C island. You did not buy idiomatic D on that core.

What “tgc” is for

We prototyped an opt-in factory named tgc (thread garbage collection; some of us have been saying “realtime GC” in conversation, which is a side-name only). It does not replace the default conservative collector. You ask for it:

./app --DRT-gcopt=gc:tgc
Mock terminal selecting tgc while the conservative factory stays registered
Same pluggable GC switch as conservative, precise, or manual.

Each attached thread gets its own arena. When Heap A collects, only Thread A’s stack, TLS, and blocks are in play. No thread_suspendAll for that cycle. Sibling GC threads keep running. Detached @nogc threads never registered in the first place.

Default GC pauses both threads; tgc pauses only the collecting thread
The latency win is pause isolation, not “faster mark-sweep.”

The picture that sells it

Think of a small studio app: a present loop that must hold 60 fps, an audio callback that must not hitch, and a decode worker that does allocate. Today, decode’s collection can still stop the shell. With tgc, decode collects locally. The window keeps presenting.

Mock studio window with a live 60 fps preview while a decode worker collects on its own heap
Mock of the architecture, not a shipping product screenshot.

What you pay

Per-thread heaps waste RAM relative to one packed global pool. Thread 1 can sit on free pages Thread 2 still needs from the OS. Moving a buffer without copying means a remote free queue on the owning thread—Thread 2 must not splice Thread 1’s free lists. Huge frames (4K video) still want an explicit shared region, which is Phase 2, not v1.

Shared global heap with a mutex and stop-the-world versus per-thread tgc heaps
Choose tgc when tail latency beats peak density.

Phase 2 is a many-to-many sketch: attach workers only to the regions they need, so collecting Region Beta does not pause a worker that never joined it. Transitive coupling can still cascade. The docs article goes into that honestly.

Partitioned GC regions: some workers share Alpha and Beta, one worker stays local-only
Not one global shared heap—attach by need.

Not the only GC in flight

SymGC (Steven Schveighoffer / Symmetry Investments; gc:sdc) is a clean-slate collector aimed at allocation throughput, the global alloc lock, and a modern layout. We Need a New GC from DConf ’24 is the public talk. That work might become a future default. tgc is a narrower experiment: pause isolation for mixed GC / @nogc processes. Complementary axes, not a turf war.

Where to look

We submitted the prototype upstream on 2 August 2026: dlang/dmd#23514. Rainer already flagged real limits (immutable sharing, header size, linear findBlock, remote-free assumptions). Those belong in review, not in this essay.

Treat v1 as design review and measurement, not a production realtime guarantee. If you already pass messages and keep the shell off the worker heap, this is the GC half of that architecture.