Using Haskell STM for Atomic Shared State: An Architecture Note
Explore how Haskell's STM provides a minimal, safe design for concurrent shared state, covering requirements, boundaries, checks, failure modes, and when to switch designs.
01 Dec 2025, 15:36 UTC

Problem
When lightweight Haskell threads need to update shared mutable state without losing consistency, naive use of IORef or MVar can lead to race conditions or complex lock ordering.
Requirements
- Atomic, composable updates to shared state.
- No explicit locks; rely on runtime conflict detection.
- Ability to wait for a condition (e.g., retry until a value reaches a threshold).
- Isolation from I/O inside the transaction.
Smallest Suitable Design
Model each piece of shared state as a TVar a from Control.Concurrent.STM. All updates occur inside an atomically block. Use retry to block until a condition holds, and orElse to compose alternatives. No additional scaffolding is required beyond the base STM library.
Trust/Data Boundaries
The atomically block creates a trust boundary: only pure computations and TVar reads/writes are permitted. Any I/O action (e.g., putStrLn, file access) inside the block results in a compile‑time error because its type is IO a, which cannot be used in the STM monad. This prevents accidental side‑effects and ensures that the transaction’s effect is limited to the TVar cells it touches.
Operational Checks
Express invariants with check :: Bool -> STM () or combine retry with a condition. Monitor for excessive retries by enabling RTS statistics (+RTS -s); a high retry count may indicate livelock or overly long transactions. Exceptions thrown inside atomically abort the transaction and propagate as a runtime exception; they can be caught with catchSTM :: Exception e => STM a -> (e -> STM a) -> STM a.
Failure Modes
- An uncaught exception inside
atomicallycauses the transaction to abort and the exception to be raised outside the block. - Deadlock cannot occur because STM resolves conflicts by aborting one of the competing transactions; however, long‑running transactions increase abort probability and waste CPU.
- Nested
atomicallycalls are prohibited and raise a runtime error (atomically was called inside atomically).
When the Design Should Change
If a transaction must perform I/O (logging, network, file writes) or requires deterministic low latency, replace STM with MVar, IORef, or a lock‑free library. For predominantly read‑only workloads, a pure functional approach (e.g., passing immutable state) may be simpler and avoid overhead.
Example Program
import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM (TVar, newTVarIO, atomically, readTVar, writeTVar)
import Control.Exception (catch, SomeException)
main :: IO ()
main = do
counter <- newTVarIO 0
let inc = atomically $ do
v <- readTVar counter
writeTVar counter (v + 1)
replicateM_ 2 $ forkIO $ replicateM_ 100000 inc
threadDelay 2000000
final <- atomically $ readTVar counter
(print final) `catch` \e -> putStrLn (show e)
Compile with GHC 9.8 or later:
ghc -O2 -threaded Main.hs -o main
Run the executable:
./main +RTS -s
Expected behavior: the program prints 200000 (two threads × 100 000 increments). The RTS summary shows transaction counts and retry information, confirming that updates were serialized without locks.
Verification Steps
- Modify the
incaction to throw an exception when the read value exceeds a threshold, for example by callingerrorinside the transaction. - Wrap the
mainaction withcatchfromControl.Exceptionto observe that the transaction aborts and the exception propagates. - Run with
+RTS -sand verify that the retry count increases when the condition triggers aborts.
Limitations and Practical Check
STM incurs overhead from transaction logs and conflict detection. For very high‑frequency updates (e.g., >10⁶ ops/sec per core) a fine‑grained MVar may be faster. To check whether STM is a bottleneck, enable RTS statistics and look for:
- High
SPARKSorGCtime unrelated to useful work. - Retry rate approaching 100 % of transactions.
If these signs appear, consider redesigning the hot path with MVar or a lock‑free structure.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.