I. Critical section

In C++, a critical section is a segment of code that is executed by multiple concurrent threads or processes and which accesses shared resources. A critical section can be any section of code where shared resources are accessed, and it typically consists of two parts: the entry section and the exit section. The entry section is where a process requests access to the critical section, and the exit section is where it releases the resources and exits the critical section.

To prevent data races in C++, critical sections can be protected using synchronization mechanisms such as mutexes, locks, and thread-safe data structures.

II. Mutex (MUTual EXclusion )

A mutex is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads. A mutex is a lockable object that is designed to signal when critical sections of code need exclusive access, preventing other threads with the same access from modifying the shared data. In C++, a mutex is implemented as a class, std::mutex, which provides two member functions: lock() and unlock().

Thread Synchroniztion with Mutex
- Thread A locks the mutex
- Thread A enters the critical section
- Thread B, C,.. wait until they can lock the mutex
- Thread A leaves the critical section
- Thread A unlock the mutex
- One of threads B,C,.. can now lock the mutex and enter the critical section.

std::mutex Class

The std::mutexclass is a synchronization primitive in C++ that can be used to protect shared data from being simultaneously accessed by multiple threads. The class provides member functions, lock(), try_lock() and unlock(), which can be used to lock, tries to lock the mutex -> returns immediately if not successful and unlock the mutex, respectively.

Example of multiple threads with mutex in C++

Example unscramble.cpp bellow:

unscramble.cpp

// Use a mutex to avoid scrambled output
#include <iostream>
#include <mutex>
#include <thread>
#include <string>

// Global mutex object
std::mutex task_mutex;

void task(const std::string& str)
{
	for (int i = 0; i < 5; ++i) {
		// Lock the mutex before the critical section
		task_mutex.lock();

		// Start of critical section
		std::cout << str[0] << str[1] << str[2] << std::endl;
		// End of critical section

		// Unlock the mutex after the critical section
		task_mutex.unlock();
	}
}

int main()
{
	std::thread thr1(task, "abc");
	std::thread thr2(task, "def");
	std::thread thr3(task, "xyz");

	thr1.join();
	thr2.join();
	thr3.join();
}

Output

abc
abc
abc
abc
abc
def
def
def
def
def
xyz
xyz
xyz
xyz
xyz
Mutex example from James
Mutex example from James

std::mutex::trylock()

The std::mutex::try_lock() function is a member function of the std::mutex class in C++ that attempts to lock the mutex without blocking. The function returns immediately and tries to acquire the lock. If the lock is acquired successfully, the function returns true, and if the lock is not acquired, the function returns false. The std::mutex::try_lock() function can be used to avoid blocking when acquiring a lock on a mutex.

// Keep trying to get the lock
while(!the_mutex.try_lock()){
	// Could not lock the mutex
	// Try again later
	std::this_thread::sleep_for(100ms);
}
	// Finally locked the mutex
	// Can now execute in the critical section

Example try_lock() the mutextry_lock_mutex.cpp bellow:

try_lock_mutex.cpp

// Example of calling try_lock() in a loop until the mutex is locked
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

using namespace std::literals;

std::mutex the_mutex;

void task1()
{
	std::cout << "Task1 trying to lock the mutex" << std::endl;
	the_mutex.lock();
	std::cout << "Task1 has locked the mutex" << std::endl;
	std::this_thread::sleep_for(500ms);
	std::cout << "Task1 unlocking the mutex" << std::endl;
	the_mutex.unlock();
}

void task2()
{
	std::this_thread::sleep_for(100ms);
	std::cout << "Task2 trying to lock the mutex" << std::endl;
	while (!the_mutex.try_lock()) {
		std::cout << "Task2 could not lock the mutex" << std::endl;
		std::this_thread::sleep_for(100ms);
	}
	std::cout << "Task2 has locked the mutex" << std::endl;
	the_mutex.unlock();
}

int main()
{
	std::thread thr1(task1);
	std::thread thr2(task2);
	
	thr1.join();
	thr2.join();
}

Output

Task1 trying to lock the mutex
Task1 has locked the mutex
Task2 trying to lock the mutex
Task2 could not lock the mutex
Task2 could not lock the mutex
Task2 could not lock the mutex
Task2 could not lock the mutex
Task1 unlocking the mutex
Task2 has locked the mutex

III. Internally Synchronized Class

When multiple threads access the same memory location concurrently, and at least one of them modifies that memory location, we need to synchronize these accesses to prevent a data race.

Wrapper for std::vector

+ std::vector acts as a memory location
- We may need to lock a mutex before calling its member function
+ Alternatively, we could write an internally synchronized wrapper for it.
+ A class which
	- Has an std::vector data member
	- Has an std::mutex data member
	- Member functions which lock the mutex before accessing the std::vector
	- Then unlock the mutex after accessing it
+ An internally synchronized class
//Very simplistic thread-safe vector class
class Vector{
	std::mutex mut;			// Mutex as private class data member
	std::vector<int> vec;	// Shared data -mutex protects access to it
	public:
		void push_back(const int& i){
			mut.lock(); 		//Lock the mutex
			vec.push_back(i);	//Critical section
			mut.unlock();		//Unlock the mutex
		}
	};

Example A class which is internally without mutex bellow:

without_mutex.cpp

// A class which is internally synchronized
// The member functions lock a mutex before they access a data member
#include <thread>
#include <mutex>
#include <vector>
#include <iostream>
#include <chrono>

using namespace std::literals;

class Vector {
	std::mutex mut;
	std::vector<int> vec;
public:
	void push_back(const int& i)
	{
		//mut.lock();

		// Start of critical section
		vec.push_back(i);

		// End of critical section
		//mut.unlock();
	}

	void print() {
		//mut.lock();

		// Start of critical section
		for (auto i : vec) {
			std::cout << i << ", ";
		}

		// End of critical section
		//mut.unlock();
	}
};

void func(Vector& vec)
{
	for (int i = 0; i < 5; ++i) {
		vec.push_back(i);
		std::this_thread::sleep_for(50ms);
		vec.print();
	}
}

int main()
{
	Vector vec;

	std::thread thr1(func, std::ref(vec));
	std::thread thr2(func, std::ref(vec));
	std::thread thr3(func, std::ref(vec));

	thr1.join(); thr2.join(); thr3.join();
}

Scramble Ouput

0, 0, 
end print section
0, 0, 
0, 0, 1, 
end print section
end print section
start print section
0, 0, 1, 1, 1, 
end print section
start print section
0, 0, 1, 1, 1, 2, 
end print section
start print section
0, 0, 1, 1, 1, 2, 2, 
end print section
start print section
0start print section, 
0, 0, 1, 1, 1, 2, 2, 2, 
0, end print section
1, 1, 1, 2, 2, 2, 
end print section
start print section
0, 0, 1073744080, 32544, 1, 2, 2, 2, 3, 
end print section
start print section
0, 0, 1073744080, 32544, 1, 2, 2, 2, 3, 3, 
end print section
start print section
0, 0, 1073744080, 32544, 1, 2, 2, 2, 3, 3, 4, 
end print section
start print section
0, 0, 1073744080, 32544, 1, 2, 2, 2, 3, 3, 4, 4, 
end print section
start print section
0, 0, 1073744080, 32544, 1, 2, 2, 2, 3, 3, 4, 4, 4, 
end print section
start print section
start print section0, 0, 1073744080, 32544, 1, 2, 2, 2, 3, 3, 4, 4, 4, 
end print section
0, 0, 1073744080, 32544, 1, 2, 2, 2, 3, 3, 4, 4, 4, 
end print section

Example A class which is internally synchronize bellow:

internal_sync_class.cpp

// A class which is internally synchronized
// The member functions lock a mutex before they access a data member
#include <thread>
#include <mutex>
#include <vector>
#include <iostream>
#include <chrono>

using namespace std::literals;

class Vector {
	std::mutex mut;
	std::vector<int> vec;
public:
	void push_back(const int& i)
	{
		mut.lock();

		// Start of critical section
		vec.push_back(i);

		// End of critical section
		mut.unlock();
	}

	void print() {
		mut.lock();

		// Start of critical section
		for (auto i : vec) {
			std::cout << i << ", ";
		}

		// End of critical section
		mut.unlock();
	}
};

void func(Vector& vec)
{
	for (int i = 0; i < 5; ++i) {
		vec.push_back(i);
		std::this_thread::sleep_for(50ms);
		vec.print();
	}
}

int main()
{
	Vector vec;

	std::thread thr1(func, std::ref(vec));
	std::thread thr2(func, std::ref(vec));
	std::thread thr3(func, std::ref(vec));

	thr1.join(); thr2.join(); thr3.join();
}

Stable Ouput

start print section
0, 0, 0, 
end print section
start print section
0, 0, 0, 
end print section
start print section
0, 0, 0, 1, 1, 
end print section
start print section
0, 0, 0, 1, 1, 1, 
end print section
start print section
0, 0, 0, 1, 1, 1, 
end print section
start print section
0, 0, 0, 1, 1, 1, 2, 
end print section
start print section
0, 0, 0, 1, 1, 1, 2, 2, 2, 
end print section
start print section
0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 
end print section
start print section
0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 
end print section
start print section
0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 
end print section
start print section
0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 
end print section
start print section
0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 
end print section
start print section
0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 
end print section
start print section
0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 
end print section
start print section
0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 
end print section

IV. Lock guard

Mutex problem with the exception is thrown

To protect a critical section, we can use a mutex or a lock. We lock the mutex before entering the critical section and unlock it afterwards. However, if an exception is thrown while the thread is in the critical section, the code will jump out of the try block into the catch block, and the code that follows will not be executed. This means that the unlock function is NEVER called, and the mutex is left in a locked state.

try{
	task_mutex.lock(); //Lock the mutex before the critical section
	//Critical section thrown an exception

	task_mutex.unlock(); // NEVER get called
}
catch(std::exeption&e ){
 ....
}
// -> MUTEX will be left locked state -> Error: device or resource busy

So, when the exception is thrown, we have the stack unwinding process:

When a thread locks a mutex, any other thread that wants to lock that mutex will wait indefinitely, causing the threads to be blocked. If any code has called join() on those blocked threads, such as themain() function, then that code will also be blocked. As a result, the entire program will be blocked, and the threads will not be able to proceed.

Drawback of std::mutex:
  • Calling lock() requires corresponding call unlock() If NOT the mutex remaining locked after the thread exits
  • Unlock must always be called, even ifThere are multiple paths through the critical section, An exception thrown
  • Relies to the programmer to get it right
  • Due to these reasons, we do not normally use std::mutex

So what should we use? -> Mutex Wrapper Class

A mutex wrapper class is a class that holds a mutex and an associated object, providing a convenient way to lock and unlock the mutex when accessing the object. the wrapper classes for mutexes in C++ provide the following benefits:

std::lock_guard

std::lock_guard is a C++ class that provides a convenient RAII-style mechanism for owning a mutex for the duration of a scoped block. std::lock_guard is a useful tool for synchronizing access to shared resources in a multithreaded environment. It provides a convenient way to lock and unlock a mutex when accessing an object, ensuring that only one thread can access the object at a time. It is simple to use and has less likelihood for incorrect use than other mutex wrapper classes.

Example use std::lock_guard to avoid scrambled output:

lock_guard.cpp

// Use std::lock_guard to avoid scrambled output
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
#include <string>

using namespace std::literals;

std::mutex print_mutex;

void task(std::string str)
{
	for (int i = 0; i < 5; ++i) {
		try {
			// Create an std::lock_guard object
			// This calls print_mutex.lock()
			std::lock_guard<std::mutex> lck_guard(print_mutex);

			// Start of critical section
			std::cout << str[0] << str[1] << str[2] << std::endl;

			// Critical section throws an exception
			throw std::exception();
			// End of critical section

			std::this_thread::sleep_for(50ms);
		}  // Calls ~std::lock_guard
		catch (std::exception& e) {
			std::cout << "Exception caught: " << e.what() << '\n';
		}
	}
}

int main()
{
	std::thread thr1(task, "abc");
	std::thread thr2(task, "def");
	std::thread thr3(task, "xyz");

	thr1.join(); thr2.join(); thr3.join();
}

Output

abc
Exception caught: std::exception
def
Exception caught: std::exception
def
Exception caught: std::exception
def
Exception caught: std::exception
def
Exception caught: std::exception
def
Exception caught: std::exception
xyz
Exception caught: std::exception
abc
Exception caught: std::exception
xyz
Exception caught: std::exception
xyz
Exception caught: std::exception
xyz
Exception caught: std::exception
xyz
Exception caught: std::exception
abc
Exception caught: std::exception
abc
Exception caught: std::exception
abc
Exception caught: std::exception

The exceptions are being caught and different threads are executing. Other threads are able to lock the mutex and perform their own critical section without scrambled output.

std::unique_lock()

In general, std::lock_guard is simpler and easier to use, while std::unique_lock is more flexible and provides more functionality. If the mutex needs to be locked for the entire scope of a block, std::lock_guard is preferred. If the mutex needs to be locked for only part of the scope of a block or if more advanced functionality is needed, std::unique_lock is preferred.

Example use std::unique_lock() to avoid scrambled output:

unique_lock.cpp

// Use std::unique_lock to avoid scrambled output
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
#include <string>

using namespace std::literals;

std::mutex print_mutex;

void task(std::string str)
{
    for (int i = 0; i < 5; ++i) {
		// Create an std::unique_lock object
        // This calls print_mutex.lock()
        std::unique_lock<std::mutex> uniq_lck(print_mutex);
		
		// Start of critical section
        std::cout << str[0] << str[1] << str[2] << std::endl;
		// End of critical section
		
		// Unlock the mutex
		uniq_lck.unlock();

        std::this_thread::sleep_for(50ms);
    } // Calls ~std::unique_lock
}

int main()
{
	std::thread thr1(task, "abc");
	std::thread thr2(task, "def");
	std::thread thr3(task, "xyz");
	
	thr1.join(); thr2.join(); thr3.join();
}

Output

abc
def
xyz
abc
def
xyz
abc
def
xyz
abc
def
xyz
abc
def
xyz

The std::unique_lock class has several constructors that allow for different ways of locking a mutex. The second argument of the constructor is optional and specifies the locking strategy. Here are some of the available options:

std::lock_guard vs std::unique_lock
  • std::unique_lock is much more flexible, but: Slower and requires slighly more storage
  • Recommendationuse lock_guard to lock mutex for entire scope, unique_lock for unlock wihin the scope, use unique_lock if you need more extra features.

References

  1. https://learn.microsoft.com/en-us/cpp/cppcx/wrl/criticalsection-class?view=msvc-170
  2. https://en.cppreference.com/w/cpp/thread/mutex/try_lock
  3. https://cplusplus.com/reference/mutex/mutex/try_lock/
  4. James Raynard, Learn Multithreading with Modern C++ Udemy.
  5. https://blog.andreiavram.ro/cpp-channel-thread-safe-container-share-data-threads/