Mutex

01 Feb 2014

#Mutual Exclusion

One of the biggest problems with concurrency is that of sharing data between threads. Development is less problematic when the data is read-only, but as soon as we have two or more threads that need to write to the data at the same time, the integrity of our data is lost and invariants are incoherent. The most common reason is due to race conditions. Once of the ways to protect your application from the problems of concurrency is to design your classes to be lock free. While this is a whole separate art in and of itself, we will talk about the more common solution to the problem, which is locking.

What we need to do is to make sure that the specific parts of the code that write to shared data can only be executed one at a time. While executing, that part of code will be mutually exclusive from the other threads. If thread A is accessing that part of code, thread B needs to wait until thread A has finished writing to the data before B can start writing. We can use C++ 11’s std::mutex object for this purpose. Lets say we need to protect a container of bank transactions:

#include <mutex>
#include <vector>

class Transactions
{
private:
    std::mutex _transactionsMutex;
    std::vector<int> _transactionsVector;
    
    
public:
    void addTransactionOfID(int transactionID);
    std::vector<int> allTransactions();
};

While you can explicitly lock and unlock a mutex:

gTransactionsMutex.lock();
//...
gTransactionsMutex.unlock();

This is not recommended because there are in fact helper objects that we can use that have been optimized to be exception-safe and help prevent deadlocking. They are std::lock_guard and std::unique_lock. Lets take a look at lock_guard first:

void Transactions::addTransactionOfID(int transactionID)
{
    //do a bunch of work
    
    {
        std::lock_guard<std::mutex> locker(_transactionsMutex);
        _transactionsVector.push_back(transactionID);
    }
    
    // do a bunch of other work
}

Here we give the lock_guard template class a std::mutex, and then in it’s constructor we pass the actual mutex we want to lock. This will lock everything from that line of code right to the end of the function for you. A very important thing to remember is to only lock the most necessary parts of the code that actually access shared data, as opposed to locking at the beginning of the function which could perform long operations; unnecessarily blocking all other threads from executing. This is why in the above section, we can wrap the lock_guard in curly braces. As soon as the braces are closed, the lock_guard goes out of scope and unlocks the mutex, giving all the other threads a chance.

If you wanted explicit control over when to lock and unlock the mutex while still having all the optimized functionally of the wrapper class, you can use std::unique_lock.

std::unique_lock<std::mutex> locker(_transactionsMutex, std::defer_lock);
//do some work
locker.lock();
//do some work
locker.unlock();

unique_lock’s default behaviour is to lock right away. We have the option to add defer_lock to the unique_lock’s constructor so that we can lock it at a later time. Then we unlock it when we are finished with the part of the code that accesses the shared data.

####Points:

Scattering locks all over the areas of your code that access shared data is not a good practice as it is harder to keep track of all those locks. It’s much better to try and keep all this functionality in one place. Good design using accessors methods is one way to solve this problem. Using getter and setter methods and only using these methods to access the data (even internally in your class) means that you can lock in one place. This avoids having to update many parts of your code if you are adding or removing locks from your code.

Also, It would be pointless to have all this protection when your interface’s getter exposes a pointer or reference to the shared data, because now any user of the class can do what they want with the data without using the accessor methods or locking the mutex. Because you have implemented accessor methods, you now have control over how the data is set and retrieved. For example, you can return copies to the data instead of pointers in the getter. Careful interface design and data encapsulation is important especially when designing concurrent programs to make sure the shared data is really protected from any user being able to access it while other threads may be writing to it at the same time.

On the topic of design, it’s a good idea to write your methods with one one entry and one exit point. Not only is this good for readability, for looking at and thinking about the flow, but also for multi threading support. Lets say a class was designed without concurrency in mind. Later the requirements changed so that it must now support concurrency. When it comes time to place locks around parts of your code (hopefully much of it is already located all in one place), you will need to rewrite a lot of your functions just to be thread-safe to prevent deadlocking if your code looks like this:

void Transactions::doSomething()
{
    // <--- need to lock here
    
	if (empty)
		return;
	//do some more work
	if (!success)
		return;
	//do some more work
            //etc
}

A return while a standard pthread mutex is locked will block the thread indefinitely because it never gets unlocked. (This is also true for threading APIs such as Cocoa’s NSThread). However, this updated function is safe, even if you locked right at the beginning right up until the end of the function:

void Transactions::doSomething()
{
    // << --- can lock here
    
	if (!empty)
	{
		//do some work
		if (success)
		{
			//do some more work
		}
	}
}

The same is true for returning values. You can keep a local ivar of the return value (optionally initializing it to some fail state at the beginning of the function) and then update it throughout your function, returning it once at the end of the function, as opposed placing returns at various stages of the function.

For protection of the shared data the mutex is locking, the copy constructor will be implicitly deleted when you add std::mutex to a class as opposed to having std::mutex as a global variable outside of the class. Generally keeping the mutex associated with the shared data inside the class is better design. This will compile:

Transactions t;

This will not:

Transactions q = t;

If you need to do the above you will need to override the copy constructor (and possibly other constructors given the c++ rule of 3) and define how you want your shared data to be copied while still being protected. The same is true for std::atomic.

A thread will not lock a mutex if it already owns it. If you do want this behaviour, check out recursive_mutex.

Terms to know:

Just for the reference, we talk in terms of locking a mutex, but the definition of a lock verses a mutex technically are different. A lock is not shared with any other process, whereas in systems programming, a mutex is a lock that can be system-wide. This can be shared memory between more than one process, which is known as the “critical region”.

A semaphore is a mutex that allows more than one thread to enter the critical region, either by counting (counting semaphore) or by an available/unavailable flag (binary semaphores). If we needed to check the status of only one single event such as a finished/unfinished task signalled from another thread and either wait or continue based on the 1 or 0, we would implement a binary rendezvous semaphore.

A spinlock is a lock that uses busy-waiting – A loop that keeps testing a flag or variable until it is of a specific value. See condition_variable.

As always, for more information about mutexes, see here.