MultiThreading
 All Modules Pages

Basic functionality - std::lock_guard

Assume there are some global data, which may be repeatedly accessed by different threads. As long as all threads are only reading the data there is no need for special synchronization. But if at least one thread may change the data while others are trying to read them you should use a mutex for controlling access:

include <mutex>
// Global mutex used for synchronization
std::recursive_mutex g_myDataMutex;
// Global data definition
SomeData g_myData;

Within each thread you have to acquire the mutex before accesssing data. When you are ready with processing data you should release the mutex to give other threads a chance to access data.

// Thread needing access to global data
// do something
...
// now needing access to global data
{
std::lock_guard<std::recursive_mutex> myLock(g_myDataMutex)
g_myData.ReadSomeValues();
g_myData.ChangeSomeValues();
} // automatic unlock of mutex
// do something else
...

As long as the mutex is locked by some other thread the code will be blocked within the constructor call of myLock. When the mutex gets available the code sequence will proceed.

Exception safety

Instead of using a mutex directly you should always use a guard object for calling lock() and unlock() methods of the mutex. Especially in case of exceptions the destructor of the guard object will ensure that the mutex is unlocked.

Recursive or non recursive locking

The sample code above uses a recursive mutex. Thus a thread already having acquired the mutex can repeatedly lock the mutex (e.g. by calling some helper functions which itself care about locking). You can use a more strict programming paradigm allowing only a single lock of a mutex regardless whether it is the same thread or another thread wanting to access the data. For this paradigm you should use std::mutex instead of std::recursive_mutex.

Repeatedly locking and unlocking - std::unique_lock

It may be necessary to repeatedly access shared data and in between give other threads a chance for data access:

// Thread needing access to global data
...
{
std::unique_lock<std::recursive_mutex> myLock(g_myDataMutex)
g_myData.ReadSomeValues();
g_myData.ChangeSomeValues();
// ## temporarily release lock of data ##
myLock.unlock();
// e.g. make some time consuming calculations
...
// ## reacquire lock of data ##
myLock.lock();
// do something else with global data
...
} // ## automatic unlock of mutex ##

Avoid long waiting for data access - std::recursive_timed_mutex

Sometimes it is useful to process data only if they are available immediately or within a short time period.

First you have to use a special kind of mutex:

std::recursive_timed_mutex g_myDataMutex;

Within the thread function trying to access global data you may be prepared for not getting access:

// ## not yet locking mutex for data access ##
std::unique_lock<std::recursive_timed_mutex> myLock(
g_myDataMutex, std::defer_lock);
if (myLock.try_lock()) // ## lock if possible ##
//or
if (myLock.try_lock_for(sd::chrono::milliseconds(10)))
{
// access data
}
else // data access was not granted
{
// do something else
}

Avoid deadlocks by locking several mutexes in one atomic step - std::lock

A typical deadlock situation may arise if different threads try to lock resources A and B in different orders. One thread may have locked resource A and waits until resource B gets free. The other thread may have locked resource B and waits until resource A gets free.

A solution to avoid this situation is to lock only both resources or none of them:

// not yet locking mutexes A and B
std::unique_lock<std::mutex> myLockA(
g_myMutexA, std::defer_lock /*##*/);
std::unique_lock<std::mutex> myLockB(
g_myMutexB, std::defer_lock);
// ## Now locking both
std::lock(myLockA, myLockB);
// here both mutexes are acquired