Switch branches/tags
Nothing to show
Find file History
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
..
Failed to load latest commit information.
Interleaving.hs
Logger.hs
MVar1.hs
MVar2.hs
MVar3.hs
PhoneBook.hs
README.org
Reminders.hs
Reminders2.hs

README.org

Notes on Concurrent Haskell

MVar

  • An MVar is 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 MVar is 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 an MVar. Modifying the state consists of taking the current value with takeMVar (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 MVar with a dummy value such as () to act as a lock on the external state, where takeMVar acquires the lock and putMVar releases it again.

  • An MVar is 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 book

Now 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.

MVar as a Building Block: Unbounded Channels