Common Concurrency Abstractions in Haskell, MVar

Posted in category haskell on 2015-07-31

Table of Contents

Haskell has many concurrency abstractions built into the base library as well as lots more in the form of libraries. This series of blog posts continueus with synchronized mutable variable, MVar.

Synchronized mutable variable, MVar

Official Control.Concurrent.MVar module documentation can be found here.

MVar is intermediate level blocking sycnhronization mechanism that allows threads to communicate with each other. Concept of synchronizing variable is similar to a box for just one single element - it is either full or empty. Therefore, logic for MVar variable is very simple:

MVar implementation is fair in such a way that it blocks threads in a FIFO queue which provides some guarantees that as long as values are repeatedly replaced in a variable, all threads are gettings eventually unblocked.

Overhead in using MVar is typically higher than with IORef. Most obvious reason is that IORef is non-blocking mechanism that utilizes compare-and-swap low level atomic processor instruction, whether MVar is a blocking mechanism that only lets single executing thread inside critical section at a time. Another reason is that MVar allows to implement any logic inside a critical section that may contain side effects. This possess another risk – locking your little transaction for too long which is not desirable in most cases.

MVar is used with the following set of key functions:

Last two exception-safe versions of functions make sure that in case of any exception in a critical section, putMVar is always called making sure that lock is released and other threads waiting for it will not stall.

Typical cross-thread communication scenarios with using MVar

MVar can be used in many different way. Here are some typical cross-thread communication scenarios:

Round-robin request handling

MVar can be used for handling requests in a round-robin fashion - next request is handled by first vacant handler. Please consider following example:

Notice that threadDelay 100000 is used after setting all values? It is there to ensure that forked thread will have a chance to complete its job before whole application will be stopped, since Haskell runtime will terminate all background threads upon the exit of the main thread including threads that have still something to do.

This program will produce following trace:

As you can see each other request is handled by next vacant handler making them alternating.

Fan-in request handling

MVar can easily be used for creating simple one-way channels when logging thread(s) put something in MVar while one other persisting thread receives it on the other side and then persists it in one way or another. A very straight forward example for such communication could be simple logger which exposes MVar while other threads write into it.

Consider following example:

This program emits following trace:

Log entries are mixed from different threads as they keep doing something.

Ensuring boundaries of data (shared state container)

MVar ensures boundaries/consistency of enclosed data. Consider following example where there is a bank account with certain amount of money on it, and how multiple threads are all trying to withraw some money from it:

This is what the program emits:

As you can see consistency of a bank account is ensured.

Ensuring boundaries of a complex critical section

It is rather typical to use MVar as a lock around critical sections in the code. Taking a value from MVar variable typically means acquiring the lock (starting a critical section) and putting changed value back into MVar variable means releasing the lock (concluding a critical section) subsequently allowing next signle thread in the queue to acquire the lock.

Now let’s take previous example and introduce another bank account. Our goal is to ensure consistency across all bank accounts this time.

This program emits following trace:

Notice, how different transactions are not overlapping each other ensuring consistency throughout multiple MVar instance and not only within individual MVar instances.

Pitfalls of using low-level concurrency abstractions

When choosing MVar, it is necessary to be mindful about certain pitfalls it is easily possible to get into.

Deadlocks

Deadlock is a situation in which several actions are waiting for each other to finish, and thus neither ever does.

In general any blocking concurrency abstraction that has separate operations for acquiring and releasing locks is susceptible to deadlocks. This is exactly what we have with MVar - takeMVar for acquiring a lock and putMVar for release it.

Consider the following example with two MVar variables and two different threads waiting until another one finishes:

Here is the log of execution of the above program:

Just to prove that the program works fine without waiting for another action to finish, comment out lines 14 and 21. The program looks like following now:

Then execution log will look way better:

This was probably simplest deadlock scenario with using MVar. There are many more other ways to deadlock your pragrams most of which are not as easy to resolve as in the above example, so be careful.

Resource management

Due to the fact that acquiring and releasing a lock are two distinct operations might lead to another kind of problem – reliable resource management. Consider the following program:

Between 12~13 lines is the critical section for Thread #1 which in this case fails. This leads to the situation when the lock is never released (line 14). As a result no other thread can acquire it again – somewhat a deadlock.

In order to avoid these kinds of errors one could either fallback to using exception-safe functions like withMVar or implement try-finally pattern manually. Here is how withMVar function is implemented in the base package:

Aside from all the interesting functions (like mask, onException) this code does a very simple thing - it makes sure that in case of any unforeseen error putMVar will anyway be called, thus a lock will be released in any case. So, let’s see how our previous example would look like with using withMVar instead of using explicit pair of takeMVar and putMVar:

This program will run with the following trace, demonstrating that this time any unforeseen errors are handled correctly with regards to releasing a shared resource (lock in this case):

References

  1. “Concurrent and Multicore Programming” chapter in Real World Haskell Book
  2. “Basic Concurrency: Threads and MVars” chapter in the exceptional Simon Marlow’s book, “Parallel and Concurrent Programming in Haskell”

“Common Concurrency Abstractions in Haskell” Series