Atomic Counters in Haskell

Our primary mechanism for managing state in Haskell is through pure functions and immutable data structures. However, there are situations where we need to manage shared mutable state. Here we’ll look at using the Control.Concurrent.STM package for atomic operations in a concurrent environment.

import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad (replicateM)

main :: IO ()
main = do
    -- We'll use a TVar to represent our (always-positive) counter.
    ops <- atomically $ newTVar 0

    -- We'll start 50 threads that each increment the counter exactly 1000 times.
    let work = replicateM_ 1000 $ atomically $ modifyTVar' ops (+1)
    threads <- replicateM 50 $ forkIO work

    -- Wait until all the threads are done.
    mapM_ takeMVar threads

    -- Here no threads are writing to 'ops', but using readTVarIO
    -- it's safe to atomically read a value even while other threads
    -- might be updating it.
    finalOps <- readTVarIO ops
    putStrLn $ "ops: " ++ show finalOps

We expect to get exactly 50,000 operations. By using STM (Software Transactional Memory), we ensure that our operations are atomic and free from race conditions.

To run the program:

$ ghc -threaded atomic-counters.hs
$ ./atomic-counters
ops: 50000

In this Haskell version:

  1. We use TVar from Control.Concurrent.STM as our atomic integer type.
  2. Instead of goroutines, we use Haskell’s lightweight threads.
  3. The atomically function is used to perform atomic operations on the TVar.
  4. We use modifyTVar' to atomically increment the counter.
  5. forkIO is used to create new threads.
  6. readTVarIO is used to safely read the final value of the counter.

This approach ensures thread-safety and eliminates the possibility of race conditions, similar to the original example.

Next, we’ll look at other concurrency primitives in Haskell for managing shared state.