Building a Concurrent Counter with Haskell's Software Transactional Memory
Learn how to use Haskell's STM primitives to safely update a shared counter from multiple threads, compile with GHC, run with threading enabled, and verify the result.
30 Jul 2025, 04:41 UTC

Desired outcome
Create a small Haskell program that spawns several threads, each incrementing a shared counter many times, and guarantees that the final counter value equals the total number of increments performed by all threads.
Prerequisites
- GHC (Glasgow Haskell Compiler) version 8.10 or newer installed and available in your
$PATH. - A Unix‑like shell (bash, zsh, etc.) or Windows PowerShell to run commands.
- Basic familiarity with Haskell syntax and the
IOmonad.
Procedure
- Set up a project directory
mkdir stm-counter && cd stm-counter - Create the source file
cat > Main.hs <<'EOF' import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.STM (TVar, newTVarIO, readTVar, writeTVar, atomically) import Control.Monad (replicateM_) main :: IO () main = do -- Shared counter initialized to zero counter <- newTVarIO 0 let incrementsPerThread = 100000 numThreads = 4 -- Spawn worker threads replicateM_ numThreads $ forkIO $ do replicateM_ incrementsPerThread $ atomically $ do val <- readTVar counter writeTVar counter (val + 1) -- Wait for all threads to finish (simple sleep; in production use MVars or async) threadDelay 2000000 finalCount <- atomically $ readTVar counter putStrLn $ "Final counter value: " ++ show finalCount putStrLn $ "Expected value: " ++ show (incrementsPerThread * numThreads) EOF - Compile with threading support and optimisation
ghc -threaded -O2 Main.hs -o stm-counterRun this command in the project directory. No special permissions are required beyond the ability to execute
ghcand write the output binary. - Execute the program with two capabilities
./stm-counter +RTS -N2The
+RTS -N2flag tells the GHC runtime to use two OS threads, enabling true parallel execution on a multicore CPU. Adjust-Nto match the number of cores you wish to utilize. - Check the result
After the program finishes, you should see output similar to:
Final counter value: 400000 Expected value: 400000If the two numbers match, the STM transactions have correctly coordinated the concurrent updates.
Expected checks
- Compilation succeeds without warnings (aside from possible
-Wmissing-signaturesif you omitted type signatures). - At runtime, the program terminates within a few seconds; no deadlock or infinite loop is observed.
- The final counter value equals the product of
incrementsPerThreadandnumThreads.
Limitations and cautions
- Long‑running transactions: If a transaction performs substantial work (e.g., reading a large data structure) inside
atomically, the runtime may retry it many times under contention, degrading performance. Keep transactions short and focused on the minimal set ofTVaraccesses. - Unsafe IO inside STM: Calling
unsafePerformIOor any other unsafe IO operation within a transaction breaks atomicity guarantees and can lead to undefined behavior. All IO must occur outsideatomically. - Deadlock risk: Although STM avoids traditional lock deadlocks, acquiring
TVars in different orders across transactions can still cause livelock‑like retries. Adopt a consistent global ordering (e.g., always read/writeTVars in the order they were created) when a transaction touches multiple variables. - Verification of retries: To confirm that the runtime is retrying transactions as expected, you can enable the event log:
./stm-counter +RTS -N2 -l
This produces a file stm-counter.eventlog that can be inspected with ghc-events show stm-counter.eventlog or visualized in ThreadScope. Look for STM events indicating retries and commits.
Recovery options
If the program produces an incorrect final count:
- Verify that you compiled with
-threaded; without it, the runtime cannot execute true parallel threads and you may see interleaving that still yields a correct count but masks performance issues. - Check that you are not accidentally using
unsafePerformIOor similar inside theatomicallyblock. - Reduce the workload per transaction (e.g., increment a single
TVarinstead of a complex structure) and re‑run. - Increase the number of capabilities (
+RTS -N4or higher) to see whether the issue persists under higher contention.
These steps help isolate whether the problem stems from misuse of STM primitives or from environmental factors such as insufficient parallelism.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.