The Lock guarantees single-threaded access, typically to some system resource.
type Lock = MVar () newLock :: IO Lock newLock = newMVar () withLock :: Lock -> IO a -> IO a withLock x = withMVar x . const
And as an example:
lock <- newLock let output = withLock . putStrLn forkIO $ do ...; output "hello" forkIO $ do ...; output "world"
Here we are creating a lock to ensure that when writing output our messages do not get interleaved. This use of MVar never blocks on a put. It is permissible, but rare, that a withLock contains a withLock inside it - but if so, watch out for deadlocks.
The Var operates on a mutable variable in a thread-safe way.
type Var a = MVar a newVar :: a -> IO (Var a) newVar = newMVar modifyVar :: Var a -> (a -> IO (a, b)) -> IO b modifyVar = modifyMVar modifyVar_ :: Var a -> (a -> IO a) -> IO () modifyVar_ = modifyMVar_ readVar :: Var a -> IO a readVar = readMVar
And as an example:
hits <- newVar 0 forkIO $ do ...; modifyVar_ hits (+1); ... i <- readVar hits print ("HITS",i)
Here we have a variable which we modify atomically, so modifications are not interleaved. This use of MVar never blocks on a put. No modifyVar operation should ever block, and they should always complete in a reasonable timeframe. A Var should not be used to protect some external resource, only the variable contained within. Information from a readVar should not be subsequently inserted back into the Var.
A barrier starts with no value, is written to once, and read one or more times.
type Barrier a = MVar a newBarrier :: IO (Barrier a) newBarrier = newEmptyMVar signalBarrier :: Barrier a -> a -> IO () signalBarrier = putMVar waitBarrier :: Barrier a -> IO a waitBarrier = readMVar
And as an example:
bar <- newBarrier forkIO $ do ...; val <- ...; signalBarrier bar val print =<< waitBarrier bar
Here we create a barrier which will contain some computed value. A thread is forked to fill the barrier, while the main thread waits for it to complete. A barrier has similarities to a future or promise from other languages, has been known as an IVar in other Haskell work, and in some ways is like a manually managed thunk. It is an error to signal a barrier more than once and a deadlock to never signal it. Since the barrier goes from empty to full, it never blocks on a put, unless you incorrectly call signal more than once.
Combining MVar Flavours - Once
The previous three MVar wrappers are the flavours of MVar which I use regularly. These can be combined into higher-level abstractions specific to certain situations. I now give two examples, intended to show how to combine these primitives.
The once function takes an action, and returns a new action. If the action is never called the argument action will never be executed, but if it is called more than once, it will only be executed once. We can write this function as:
once :: IO a -> IO (IO a) once act = do var :: Var (Maybe (Barrier a)) <- newVar Nothing return $ join $ modifyVar var $ \v -> case v of Nothing -> do b <- newBarrier; return (Just b, do x <- act; signalBarrier b x; return x) Just b -> return (Just b, waitBarrier b)
Here we create a variable to store the result, whose state is either Nothing (we have not yet started computing) or Just a barrier (we have started computing, use this barrier to get the result out). I have found 'join $ modifyVar' is a common idiom, used to defer a blocking action (often waitBarrier) until after a modifyVar has completed, ensuring we preserve our invariant of not blocking inside a modifyVar. When running the resulting action, if the variable is a Nothing we create a new barrier, store it, and then start an action (after leaving the modifyVar) to compute the result, signal the barrier and return. If we already have a barrier, we just wait for this barrier.
[Note that you can implement once in terms of MVar directly, using only one MVar, but that violates the simple rules of the restricted MVars - rightly so, you have to use the MVar empty state to mean both atomic access to shared state, and to mean computation in progress.]
Combing MVar Flavours - Queue
As another practical example of using these restricted MVars, let us consider a special kind of queue. Message arrive individually, but are collected in bulk. When someone tries to retrieve message, if there are any messages waiting they are sent immediately. If there are no messages, the read blocks until either a message arrives or until a new reader arrives, in which case the old reader is sent away with nothing. This can be implemented as:
type Queue a = Var (Either [a] (Barrier [a])) arrive :: Queue a -> a -> IO () arrive q x = modifyVar_ q $ \q -> case q of Left xs -> return $ Left $ xs ++ [x] Right b -> do signalBarrier b [x]; return $ Left  collect :: Queue a -> IO [a] collect q = join $ modifyVar q $ \q -> case q of Left xs@(_:_) -> return (Left , return xs) _ -> do case q of Right b -> signalBarrier b ; _ -> return () b <- newBarrier return (Right b, waitBarrier b)
The type of Queue tells us most of what we need to know about the invariants - Queue has a mutable state, which is either Left (zero or more messages waiting) or a Right (someone waiting to collect messages). If we had used MVar instead of both Var and Barrier, the invariant and design would be far less clear. With these invariants clearly stated, the code just follows directly.
Creating New Flavours
I find the three MVar wrappers (Lock, Var, Barrier) much easier to understand since the rules are simpler, making maintenance easier. I have also found that most projects benefit from higher-level abstractions in some places. As an example, I defined Queue in one recent project, and Shake defines a Resource type, on top of which the resources feature is implemented. Concurrency is hard, but robust abstractions split the complexity, and thus simplify the programs.