Mutexes in Haskell

In the previous example, we saw how to manage simple counter state using atomic operations. For more complex state, we can use a mutex to safely access data across multiple threads.

import Control.Concurrent
import Control.Concurrent.MVar
import qualified Data.Map.Strict as Map
import Control.Monad (replicateM_)

data Container = Container
    { counters :: MVar (Map.Map String Int)
    }

newContainer :: IO Container
newContainer = do
    countersMVar <- newMVar $ Map.fromList [("a", 0), ("b", 0)]
    return $ Container countersMVar

inc :: Container -> String -> IO ()
inc container name = modifyMVar_ (counters container) $ \m ->
    return $ Map.insertWith (+) name 1 m

doIncrement :: Container -> String -> Int -> IO ()
doIncrement container name n = replicateM_ n $ inc container name

main :: IO ()
main = do
    c <- newContainer
    
    -- Run several threads concurrently; note
    -- that they all access the same Container,
    -- and two of them access the same counter.
    threads <- mapM forkIO
        [ doIncrement c "a" 10000
        , doIncrement c "a" 10000
        , doIncrement c "b" 10000
        ]
    
    -- Wait for the threads to finish
    mapM_ takeMVar threads
    
    -- Print the final state of the counters
    finalCounters <- readMVar (counters c)
    print finalCounters

In this Haskell version, we use MVar to implement mutual exclusion. The Container type holds an MVar containing a Map of counters. The inc function uses modifyMVar_ to safely update the counter values.

Note that in Haskell, we don’t need to explicitly unlock the mutex, as modifyMVar_ takes care of that for us. This is similar to using defer in the original example.

The doIncrement function increments a named counter in a loop, similar to the original example.

In the main function, we create three threads using forkIO, each calling doIncrement with different parameters. We then wait for all threads to finish using takeMVar.

Running the program shows that the counters are updated as expected:

$ runhaskell mutexes.hs
fromList [("a",20000),("b",10000)]

Next, we’ll look at implementing this same state management task using only threads and channels.