Mutexes

A mutex is a global variable that multiple tasks can access. Before entering code that you do not want to execute concurrently, a task should request a lock on the mutex. If the mutex is already locked, the task is stalled waiting. Once the lock is granted, the task can proceed. The task should proceed quickly and release the lock on the mutex promptly to avoid unnecessary stalls in other tasks.

If a mutex creates a lot of lock requests, it is considered highly contested. Highly contested locks will result in many tasks being stalled and a sharp reduction in program scalability. Avoiding highly contested locks through careful program design is important.

A mutex, used properly, ensures that no task reads or writes a variable or other resource when another task is writing it. Intel Threading Building Blocks mutexes work with generic programming in C++, even in the presence of exceptions. Meeting all these requirements is no small feat and takes some consideration to ensure their proper usage.

In Threading Building Blocks, mutual exclusion is implemented by classes known as mutexes and locks. A mutex is an object on which a task can acquire a lock. Only one task at a time can have a lock on a mutex; other tasks have to wait their turn.

Mutual exclusion controls how many tasks can simultaneously run a region of code. In general, you protect a region of code from concurrency when that code reads or writes a small amount of memory, which is generally interrelated by a particular operation you want to effect.

Consider a simple way to write code to allocate a node from a list of available nodes:

	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;
	}

The acquire method waits until it can acquire a lock on the mutex FreeListMutex; the release method releases the lock. It is recommended that you add extra braces where possible, to clarify for future maintainers which code is protected by the lock:

	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;
	}

If you are familiar with C interfaces for locks, you may be wondering why Threading Building Blocks does 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. In the version of AllocateNode that uses acquire and release, the explicit release causes the lock to be released before its scope ends, so when the scope ends and the destructor runs, it sees that the lock was released and does nothing.

All mutexes in Threading Building Blocks have a similar interface, which not only makes them easier to learn, but also 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 next example. That way, you can change the type of the lock later without having to edit the rest of the code. In the example, you could replace the typedef with typedef queuing_mutex FreeListMutexType, and the code would still be correct.

The simplest mutex is the spin_mutex. A task trying to acquire a lock on a busy spin_ mutex waits until it can acquire the lock. A spin_mutex is appropriate when the lock is held for only a few instructions.

For instance, the code in Example 7-1 uses a mutex that ensures only one task has access at a time. Five lines of code were added in Example 7-1 to the sequential code to provide the proper mutual exclusion to make this code thread-safe: lines 1, 3, 4, 9, and 20.

Example 7-1. SpinMutex example

 1 #include "tbb/spin_mutex.h"
 2 Node* FreeList;
 3 typedef spin_mutex FreeListMutexType;
 4 FreeListMutexType FreeListMutex;
 5
 6 Node* AllocateNode( ) {
 7     Node* n;
 8     {
 9        FreeListMutexType::scoped_lock mylock(FreeListMutex);
10        n = FreeList;
11        if( n )
12            FreeList = n->next;
13    }
14    if( !n )
15        n = new Node( );
16    return n;
17 }
18
19 void FreeNode( Node* n ) {
20     FreeListMutexType::scoped_lock mylock(FreeListMutex);
21     n->next = FreeList;
22     FreeList = n;
23 }

The *this constructor for scoped_lock waits until there are no other locks on the FreeListMutex mutex. The destructor releases the lock. The destructor runs at the closing brace, which terminates the scope of the lock; hence the name scoped lock. This interaction with the compiler also explains the unusual braces inside the AllocateNode routine. Their role is to keep the lifetime of the lock as short as possible so that other waiting tasks can get their chance as soon as possible. Without the extra braces, the scope of the lock in this example would be the entire routine.

Tip

Be sure to name the lock object; otherwise, it will be destroyed too soon because C++ compilers are allowed to eliminate unnamed objects. For example, if the creation of the scoped_lock object in the example is changed to:

	FreeListMutexType::scoped_lock (FreeListMutex);

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.8.247