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:
- We use
TVar
fromControl.Concurrent.STM
as our atomic integer type. - Instead of goroutines, we use Haskell’s lightweight threads.
- The
atomically
function is used to perform atomic operations on theTVar
. - We use
modifyTVar'
to atomically increment the counter. forkIO
is used to create new threads.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.