Notes on Concurrent Haskell
MVar
- An
MVaris a one-place channel, which means that it can be used for passing messages between threads, but it can hold at most one message at a time. - An
MVaris a container for shared mutable state. For example, a common design pattern in Concurrent Haskell, when several threads need read and write access to some state, is to represent the state value as an ordinary immutable Haskell data structure stored in anMVar. Modifying the state consists of taking the current value withtakeMVar(which implicity acquires a lock), and then placing a new value back in the MVar with putMVar (which implicitly releases the lock again).Sometimes the mutable state is not a Haskell data structure; it might be stored in C code or on the filesystem, for example. In such cases, we can use an
MVarwith a dummy value such as()to act as a lock on the external state, wheretakeMVaracquires the lock andputMVarreleases it again. - An
MVaris a building block for constructing larger concurrent Datastructures.
MVar as a Simple Channel: A Logging Service
The logging service will have the following API:
data Logger
initLogger :: IO Logger
logMessage :: Logger -> String -> IO ()
logStop :: Logger -> IO ()Having Logger be a value that we pass around rather than a globally known
top-level value is good practice; it means we could have multiple loggers, for
example.
logStop has an extra requirement: it must not return until the logging service
has processed all outstanding requests and stopped.
…
Does this logger achieve what we set out to do? The logMessage function can
return immediately provided the MVar is already empty, and then the logger
will proceed concurrently with the caller of the logMessage. However, if there
are multiple threads trying to log messages at the same time, it seems likely
that the logging thread would not be able to process the messages fast enough
and most of the threads would get blocked in logMessage while waiting for the
MVar to become empty. This is because MVar is only a one-place channel. If
it could hold more messages, we would gain greater concurrency when multiple
threads need to call logMessage simultaneously.
In MVar as a Building Block: Unbounded Channels, we will see how to use MVar
to build fully buffered channels.
MVar as a Container for Shared State
insert :: PhoneBookState -> Name -> PhoneNumber -> IO ()
insert (PhoneBookState m) name number = do
book <- takeMVar m
putMVar m (Map.insert name number book)If we were to do many insert operations consecutively, the Mvar would build up
a large chain of unevaluated expressions, which could create a space leak.
$! evals the argument strictly before applying the function:
insert :: PhoneBookState -> Name -> PhoneNumber -> IO ()
insert (PhoneBookState m) name number = do
book <- takeMVar m
putMVar m $! Map.insert name number bookNow we hold the lock until Map.insert has completed, but there is no risk of a
space leak.
To get brief locking and no space leaks, we need to use a trick:
insert :: PhoneBookState -> Name -> PhoneNumber -> IO ()
insert (PhoneBookState m) name number = do
book <- takeMVar m
let book' = Map.insert name number book
putMVar m book'
seq book' (return ())With this sequence, we’re storing an unevaluated expression in the MVar, but
it is evaluated immediately after the putMVar. The lock is held only briefly,
but now the thunk if also evaluated so we avoid building up a long chain of
thunks.