Mutual Exclusion

Mutual Exclusion#

Mutual exclusion controls how many threads can simultaneously run a region of code. In oneAPI Threading Building Blocks (oneTBB), mutual exclusion is implemented by mutexes and locks. A mutex is an object on which a thread can acquire a lock. Only one thread at a time can have a lock on a mutex; other threads have to wait their turn.

The simplest mutex is spin_mutex. A thread trying to acquire a lock on a spin_mutex busy waits until it can acquire the lock. A spin_mutex is appropriate when the lock is held for only a few instructions. For example, the following code uses a mutex FreeListMutex to protect a shared variable FreeList. It checks that only a single thread has access to FreeList at a time.

Node* FreeList;
typedef spin_mutex FreeListMutexType;
FreeListMutexType FreeListMutex;


Node* AllocateNode() {
    Node* n;
    {
        FreeListMutexType::scoped_lock lock(FreeListMutex);
        n = FreeList;
        if( n )
            FreeList = n->next;
    }
    if( !n )
        n = new Node();
    return n;
}


void FreeNode( Node* n ) {
    FreeListMutexType::scoped_lock lock(FreeListMutex);
    n->next = FreeList;
    FreeList = n;
}

The constructor for scoped_lock waits until there are no other locks on FreeListMutex. The destructor releases the lock. The braces inside routine AllocateNode may look unusual. Their role is to keep the lifetime of the lock as short as possible, so that other waiting threads can get their chance as soon as possible.

Caution

Be sure to name the lock object, otherwise it will be destroyed too soon. For example, if the creation of the scoped_lock object in the example is changed to

FreeListMutexType::scoped_lock (FreeListMutex);

then the scoped_lock is destroyed when execution reaches the semicolon, which releases the lock before FreeList is accessed.

The following shows an alternative way to write AllocateNode:

Node* AllocateNode() {
    Node* n;
    FreeListMutexType::scoped_lock lock;
    lock.acquire(FreeListMutex);
    n = FreeList;
    if( n )
        FreeList = n->next;
    lock.release();
    if( !n )
        n = new Node();
    return n;
}

Method acquire waits until it can acquire a lock on the mutex; method release releases the lock.

It is recommended that you add extra braces where possible, to clarify to maintainers which code is protected by the lock.

If you are familiar with C interfaces for locks, you may be wondering why there are not simply acquire and release methods on the mutex object itself. The reason is that the C interface would not be exception safe, because if the protected region threw an exception, control would skip over the release. With the object-oriented interface, destruction of the scoped_lock object causes the lock to be released, no matter whether the protected region was exited by normal control flow or an exception. This is true even for our version of AllocateNode that used methods acquire and release the explicit release causes the lock to be released earlier, and the destructor then sees that the lock was released and does nothing.

All mutexes in oneTBB have a similar interface, which not only makes them easier to learn, but enables generic programming. For example, all of the mutexes have a nested scoped_lock type, so given a mutex of type M, the corresponding lock type is M::scoped_lock.

Tip

It is recommended that you always use a typedef for the mutex type, as shown in the previous examples. That way, you can change the type of the lock later without having to edit the rest of the code. In the examples, you could replace the typedef with typedef queuing_mutex FreeListMutexType, and the code would still be correct.