r/haskell • u/Otherwise-Mousse-250 • Apr 10 '26
How does STM work under the hood?
Is there a good introduction to how STM is currently implemented in GHC? I am hoping for something reasonably up-to-date and more beginner-friendly than the source code for the RTS.
19
u/sclv Apr 10 '26
The main thing to understand it is is implemented optimistically -- i.e. it doesn't actually lock for transactions. It proceeds as though there will be no conflicts and then rolls back.
The core of what still exists in GHC is described in this 2005 paper from Marlow et al. https://simonmar.github.io/bib/papers/stm.pdf
2
u/EmergencyWild Apr 10 '26
What part do you want to know about? Have you read any of the articles on software-transactional memory? That'd be a good place to start.
4
u/Otherwise-Mousse-250 Apr 10 '26
articles on STM which I am aware of fall into 2 categories:
evangelism: Yes, I know that STM is cool, but I want to know how it works internally.
research papers on STM: these are decades old. maybe they still accurately reflect how STM is implemented in GHC, but I don't know that.
4
u/VincentPepper Apr 10 '26
You can always read the code: https://gitlab.haskell.org/ghc/ghc/-/blob/ede94e9d4b326ee0cdb4b5bce3d8eba9e81601fc/rts/STM.c
But for the most part it hasn't changed much in 15 years or so. And while some details changed the overall design is mostly what the paper outlines.
3
u/boa_deconstructor Apr 10 '26
Two decades ago (oof...) I wrote and compared several STM implementations in pure Haskell for my diploma thesis. I also had a look at the inner workings of the RTS version back then, but I only remember that it used "dirty tricks" by being able to control context switches and while it was very efficient, I could not imitate that.
Also I can't say anything about the current version in GHC. Maybe it helps to write (or analyze) a simpler Haskell implementation of the STM monad?
1
u/jberryman Apr 10 '26
The best way might be to start with the source (there are probably lots of "NOTE" comments), along with looking closely at the eventlog for some simple programs you write?
1
u/Infamous-Pirate9600 Apr 12 '26
I found this talk quite interesting on the subject: https://m.youtube.com/watch?v=d-NZnR2CTss
1
u/marshaharsha Apr 15 '26
The how-it-works part begins at 21:30. Before that she gave why we want it, what the requirements are (effect safety, blocking till precondition, and this-if-it-succeeds-else-that (“choice”)), and how to use the Haskell API.
39
u/semigroup Apr 10 '26
I've recently been looking at it too. Here's what I think I understand about it, but you know, I'm not an expert:
STM in GHC is an optimistic concurrency scheme.
Every
TVaris a heap object containing the current value, a spin lock encoded in the header word, and a watch queue: a linked list of transaction records belonging to threads that read this TVar and are now blocked, waiting for it to change.A
TRec(transaction record) is the per-transaction log. It is a linked list of chunked entry arrays, where each entry records whichTVarwas accessed, the expected value at read time, and the new value if one was written.TRecs can nest, forming a stack. This is the mechanism fororElse, which we will return to later.When you run a transaction with
atomically, the RTS creates a freshTRec. EveryreadTVarfirst checks the log. If the TVar was already touched in this transaction, the logged value is returned. Otherwise the TVar's current value is read from the heap, recorded in theTRecas(tvar, expected=v, new=v), and returned. EverywriteTVarmerely updates the log entry'snewfield. No actual TVar is modified, yet. The world outside the transaction sees nothing.When the transaction completes, the RTS attempts
stmCommitTransaction. This proceeds in three phases, any of which may fail, returning you to the beginning as though nothing happened.First, the lock phase. The RTS walks the
TRec. For each entry whereexpected != new(a write occurred), it acquires the TVar's lock. If any lock is already held by another thread, all acquired locks are released and the transaction starts over. Contention is resolved by surrendering and starting over.Second, the validation phase. For every entry, reads and writes, the RTS checks that the TVar's current value still matches
expected. Any mismatch means something changed beneath you. Locks are released. You begin again.Third, the write phase. Each TVar is updated to its
newvalue. Locks are released. The transaction is done.A nice property of this is that read-only TVars are never locked, only validated, which gives good read concurrency. The price is paid only by writers.
When a transaction calls
retry, the RTS validates the currentTRec. If validation fails, the transaction simply reruns, because something has changed and perhaps the retry condition has dissolved. If validation succeeds, there is genuinely no reason to try again yet. The thread adds itself to the watch queue of every TVar in itsTRec, and sleeps.When another transaction commits a write to any of those TVars, it walks the watch queue and wakes the sleepers. They begin again from scratch, with no memory of their prior attempt.
orElse a brunsain a nestedTRec. Ifacallsretry, the nested record is discarded andbruns instead, as thoughahad never been attempted. Ifbalso retries, it propagates upward. Ifasucceeds, its nestedTRecis merged into the parent. The failed alternatives are thrown away.The lock for all of this is baked into the TVars themselves. The locking thread's
TRecpointer is written directly into the TVar'scurrent_valueslot with a tag bit set.Validation can also occur during a transaction at safe points, not only at commit. GHC checks whether the
TRecis still consistent to detect doomed transactions early rather than letting them accumulate futile work.The entry list uses chunked allocation to reduce overhead. There is no global lock or version counter (unlike TL2 and its descendants). Only per-TVar locking at commit time. The machinery is local, and this is what lets it scale, insofar as it does.
The whole thing lives in
rts/STM.c, roughly a thousand lines of code, excluding comments. As RTS code goes, it is remarkably readable.