www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Opt-in thread-local GC (tgc): per-thread heaps without global STW

reply Ryan Johnson <ryan.johnson.code gmail.com> writes:
Hi all,

I've been thinking about something awkward in how D handles 
memory and threads.

In D, threads mostly keep their own data by default, and 
std.concurrency already nudges you toward message-passing instead 
of sharing everything. That feels a lot like little independent 
workers talking to each other.

But the garbage collector still acts like one big shared junk 
drawer for the whole program. When any thread needs a cleanup, 
the GC freezes every registered thread for a moment — even 
threads you carefully wrote as  nogc so they could stay smooth 
and realtime.

So I prototyped an optional collector called tgc (thread garbage 
collection). Think of it as “each thread gets its own mini heap 
and cleans up its own mess.” I might call that idea a “realtime 
GC,” but I'm using "TGC" or "tgc" in usage notes.

You turn it on like this:

   ./app --DRT-gcopt=gc:tgc

What it tries to do:
- Give each thread its own heap
- Let a thread collect garbage without pausing the other threads
- Leave the normal default GC alone unless you ask for tgc

First version is deliberately careful: prefer copying data or 
sending immutable messages between threads. If you hand a block 
of memory from one thread to another, the owning thread still 
cleans it up later. Fancy “shared regions between only some 
threads” is planned for later, not in this first cut.

Links:

- Longer explanation: 
https://dlang-supplemental.github.io/docs/docs/blog/thread-local-gc-tgc.html
- Short news note: 
https://dlang-supplemental.github.io/docs/docs/news/tgc-upstream-pr.html

Important: this is not “replace D’s default GC.” It’s an opt-in 
path for apps that care about pauses. Past forum discussions 
worried that built-in thread-local heaps get messy with `shared` 
/ `immutable`; making this pluggable and optional keeps that risk 
small.

I’d love feedback on the idea, what’s too limited in v1, and 
whether this should become a formal DIP. Comment on the PR or 
reply here.

Thanks,
Ryan / dlang-supplemental
Aug 02
parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 03/08/2026 6:36 PM, Ryan Johnson wrote:
 Hi all,
 
 I've been thinking about something awkward in how D handles memory and 
 threads.
 
 In D, threads mostly keep their own data by default, and std.concurrency 
 already nudges you toward message-passing instead of sharing everything. 
 That feels a lot like little independent workers talking to each other.
 
 But the garbage collector still acts like one big shared junk drawer for 
 the whole program. When any thread needs a cleanup, the GC freezes every 
 registered thread for a moment — even threads you carefully wrote as 
  nogc so they could stay smooth and realtime.
Yes, without going into write barriers, this is the primary solution to the problem. The secondary solution is to go concurrent, fork/snapshot. Basically throw Copy On Write at the MMU level on and go read the old pages. The current GC only supports fork and StW.
 So I prototyped an optional collector called tgc (thread garbage 
 collection). Think of it as “each thread gets its own mini heap and 
 cleans up its own mess.” I might call that idea a “realtime GC,” but I'm 
 using "TGC" or "tgc" in usage notes.
 
 You turn it on like this:
 
    ./app --DRT-gcopt=gc:tgc
 
 What it tries to do:
 - Give each thread its own heap
 - Let a thread collect garbage without pausing the other threads
 - Leave the normal default GC alone unless you ask for tgc
- Thread unsafe
 First version is deliberately careful: prefer copying data or sending 
 immutable messages between threads. If you hand a block of memory from 
 one thread to another, the owning thread still cleans it up later. Fancy 
 “shared regions between only some threads” is planned for later, not in 
 this first cut.
Immutable is only useful here if that memory is in read only memory allocated by loader. In other words, global non-TLS memory. Copying isn't a notion here either, you would need to allocate it using the destination threads GC instance. It would need to be a deep copy not a shallow copy. I do want to be very clear on this, message passing is not provide thread safety. It relies on language features to be safe. - Ownership transfer system - Guarding by mutex - Immutable We only have the last, so right now message passing is for all intents and purposes an unsafe practice in D. Which is not good enough.
 Links:

 - Longer explanation: https://dlang-supplemental.github.io/docs/docs/ 
 blog/thread-local-gc-tgc.html
 - Short news note: https://dlang-supplemental.github.io/docs/docs/news/ 
 tgc-upstream-pr.html
 
 Important: this is not “replace D’s default GC.” It’s an opt-in path
for 
 apps that care about pauses. Past forum discussions worried that built- 
 in thread-local heaps get messy with `shared` / `immutable`; making this 
 pluggable and optional keeps that risk small.
It is not thread safe. It is inherently a major risk. Nothing can utilize it and be thread safe (whilst sticking to GC memory), therefore not mergable.
 I’d love feedback on the idea, what’s too limited in v1, and whether 
 this should become a formal DIP. Comment on the PR or reply here.
 
 Thanks,
 Ryan / dlang-supplemental
Another design I came up with years ago was a graduation. It comes with the realization that the problem inherent to scanners is one of a many to many relationship. - Number of blocks to scan in. - Number of blocks to scan for. You can't decrease both, but you can decrease one of them based upon where you do it. Have a thread local scanner and heap. Once the stack, TLS, and heap reachable from those two isn't reached anymore you move that block to the global heap. The global scanner then looks at all blocks of memory in program, scans for only the blocks it thinks *could* be free. The global scanner still has to do one of the approaches I described above. Nice and thread safe. But will take longer to collect I expect.
Aug 03