Home

01 Feb 2014

Mutual Exclusion in C++

In the previous article, you learned how to add multi-threading to your application. In this article, you’ll learn about potential risks of concurrency and how to avoid them.

Say you have a CPU miner for Bitcoin. It takes advantage of concurrency to seperate work retreival and submission. You’ll want to make sure of the accuracy and correctness of that data.

One of the biggest problems with concurrency is that of sharing data between threads. As soon as more than one thread writes to the same data at the same time, a race condition can occur. Race conditions cause data corruption vulnerabilities. For example, an attacker might be able to take advantage of a time gap between the time of check and the time of use of a flag. When you apply this to security related code, the attacker might be able to change privilege or authentication status.

Some of the recent iOS jailbreaks exploit race conditions, but it’s not only limited to deliberate attacks. Think about a banking application with multiple transactions at the same time. One transaction may rewrite a previous one. Concurrent development is less problematic when you make data read-only. As soon as you have two or more threads that need to write to the data at the same time, you’ll lose the integrity of your.

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, you’ll learn a common solution to the problem, which is locking.

What you need to do is to make sure you execute specific parts of the code that write to shared data 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. You can use C++ 11’s std::mutex object for this purpose. For the bank example, you 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();
};

You can explicitly lock and unlock a mutex:

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

There are helper objects you can use. C++ has optimized them and they are exception-safe. That helps prevent deadlocking. They are std::lock_guard and std::unique_lock. You’ll 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 you give the lock_guard template class a std::mutex, and then in it’s constructor pass the actual mutex you want to lock. This will lock everything from that line of code right to the end of the function for you.

Locking Necessary Parts of Code

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; blocking all other threads from executing. The larger part of code that’s locked, that code is not concurrent. This is why in the above section, you wrapped the lock_guard in curly braces. As soon as you close the braces, the lock_guard goes out of scope and unlocks the mutex, giving all the other threads a chance to do work.

If you wanted explicit control over when to lock and unlock the mutex while still having all the optimizations 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 behavior is to lock right away. You have the option to add defer_lock to the unique_lock’s constructor so that you can lock it at a later time. Then you unlock it when you’re finished with the part of the code that accesses the shared data.

Secure Coding Best Practice

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

It would be pointless to have all this protection when your interface’s getter exposes a pointer or reference to the shared data - 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 are important when designing concurrent programs. It makes sure you protect the shared data from users accessing it while other threads are 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. Say you designed a class 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 so they’re 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 you locked a standard pthread mutex 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, initializing it to a fail state at the beginning of the function. 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. Generally keeping the mutex associated with the shared data inside the class is better design than a global variable. 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 other constructors given the C++ Rule of Three). That way, you can define how you want to copy your shared data while protecting it. The same is true for std::atomic.

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

Understanding Concurrency Terms

Normally you’ll talk in terms of locking a mutex, but the definition of a lock verses a mutex 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 you 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, you 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 for more information.

Now that you’ve learned to synchronize data, you can use the concept to coordinate the order of concurrent work. Check out the Condition Variables article to learn how to do that. For more information about mutexes, check out the mutex reference.