Mutex Lock in C++
A mutex, also known as mutual exclusion, is a synchronization primitive used in C++ to avoid concurrent access to shared resources by different threads. The simultaneous use of a shared resource by many threads may cause data corruption or strange behavior. Race problems can be avoided by using mutex locks to guarantee that only one thread can access the protected resource at a time.
Mutexes operate under the mutual exclusion principle, requiring a thread first to get the Mutex lock to access a shared resource. The requesting thread will be stopped (put to sleep) if another thread has already taken the lock and hasn't been released. Awaiting threads can move forward once the thread holding the lock completes its crucial part of the code (the area where the shared resource is accessed).
Syntax:
Include the necessary header file:
#include <mutex>
Declare a mutex object:
mutex myMutex;
If you require simple lock and unlock behavior, use lock_guard:
lock_guard<mutex> lock(myMutex);
Working of Mutex
A mutex lock normally operates as follows:
- Locking (Acquiring) the Mutex: A thread tries to lock the Mutex using a lock function, such as C++'s mutex::lock(), when it needs to access the shared resource. The locking thread successfully gets the Mutex if another thread is not holding it before performing the crucial code section.
- Mutex State: There are two possible states for mutexes: locked and unlocked. The crucial region is protected and can only be accessed by the thread that successfully locked the Mutex, setting it to the "locked" state.
- Blocking: If a thread tries to lock a mutex that another thread has already locked, it will be blocked, put to sleep, or forced to wait patiently for the Mutex to become available (unlocked). This strategy ensures that just one thread will ever be able to access the crucial part.
- Unlocking: Unlocking the Mutex When a thread has done running the critical section and no longer needs exclusive access to the shared resource, the Mutex is released using the C++ mutex::unlock() method. It is possible to return the Mutex to its initial " unlocked state," enabling other waiting threads to carry on.
- Release of the Blocked Threads: If there are any blocked threads, they will be awoken when a thread unlocks the Mutex and allows them to obtain the Mutex and enter the critical region. The precise thread scheduling order could be non-deterministic and vary depending on the system.
Example 1:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex myMutex; // declare mutex lock
void action(const string& name) {
lock_guard<mutex> lock(myMutex); // lock the mutex when entering the function
// critical section: Only one thread can execute this block of code at a time
for (int i = 0; i < 2; ++i) {
cout << "Thread " << name << " is now running " << std::endl;
}
//The Mutex will be automatically released when 'lock' goes out of scope (function exits)
}
int main() {
thread thr1(action, "Alice");
thread thr2(action, "Bob");
thr1.join();
thr2.join();
return 0;
}
Output:
![Mutex Lock in C++](https://static.tutorialandexample.com/cpp/mutex-lock-in-cpp1.png)
Explanation:
- The critical area will be secured by a mutex object called myMutex, which is defined.
- Void action(const string & name) is established that the function action accepts a constant reference to a std::string as an argument. This function represents the job that each thread will complete.
- MyMutex is passed as the parameter to a lock_guard constructed inside the action function. The Mutex is automatically locked upon construction by this lock_guard and unlocked when it leaves the function's scope. Mutex lock ensures that only one thread can simultaneously operate on the crucial region.
- Before entering the critical part, the myMutex is locked using a std::lock_guard inside the action method.
- The crucial section: The loop generates a message indicating that the thread is active inside the action function. Due to the mutex lock and the fact that the loop is in the crucial area, only one thread will be allowed to run this loop concurrently.
- Two threads are generated, thr1 and thr2, and they both call the action function with different names ("Alice" and "Bob," respectively).
- thr1.join(); thr2.join(): Before the program terminates, the main function waits for threads thr1 and thr2 to complete their execution using the join() function.
- Following that, the program returns 0, signifying a successful run.
Overall, the above code shows how the mutex lock provides synchronized access to the shared resource (the console output) by ensuring that the critical area inside the action function is executed securely by one thread at a time.
Example 2:
#include <iostream>
#include <thread>
#include <mutex>
std::thread workerThreads[2]; // Array to store two worker threads
int count = 0; // Shared count variable
std::mutex accessMutex; // Mutex for protecting the shared count
//function executed by each worker thread
void doTask()
{
// Lock the Mutex to gain access to the shared resource (count)
std::unique_lock<std::mutex> lock(accessMutex);
unsigned long i = 0;
count += 1; // Increment the shared count
std::cout << "\n Task " << count << " has started\n"; // Print the start message
// Simulate some work by running a loop (not a real delay)
for (i = 0; i < 0xFFFFFFFF; i++)
;
std::cout << "\n Task " << count << " has finished\n"; // Print the finish message
// Unlock the Mutex to release access to the shared resource (count)
lock.unlock();
}
int main()
{
int i = 0;
// Create two worker threads and store them in the 'workerThreads' array
while (i < 2) {
workerThreads[i] = std::thread(doTask); // Add std:: before thread
i++;
}
// Wait for both worker threads to finish their tasks before continuing
for (i = 0; i < 2; i++) {
workerThreads[i].join();
}
return 0;
}
Output:
![Mutex Lock in C++](https://static.tutorialandexample.com/cpp/mutex-lock-in-cpp2.png)
The C++ code uses two worker threads to carry out a task in parallel. It uses a mutex (accessMutex) to safeguard the shared variable count. Each thread prints start and end messages for each task, increases the count, and simulates labor. Data races are prevented by the Mutex, which guarantees secure access to the shared resource. The program returns 0 once both threads have completed their work.
Example 3:
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
std::vector<int> g_data;
std::mutex g_data_mutex;
void appendData(int threadId, int numIterations) {
for (int i = 0; i < numIterations; ++i) {
std::lock_guard<std::mutex> guard(g_data_mutex);
g_data.push_back(threadId);
}
}
int main() {
const int numThreads = 2;
const int numIterations = 5;
std::thread threads[numThreads];
for (int i = 0; i < numThreads; ++i) {
threads[i] = std::thread(appendData, i + 1, numIterations);
}
for (int i = 0; i < numThreads; ++i) {
threads[i].join();
}
// safe to access g_data without lock now, as the threads are joined
for (const auto& item : g_data) {
std::cout << item << " ";
}
std::cout << std::endl;
return 0;
}
Output:
![Mutex Lock in C++](https://static.tutorialandexample.com/cpp/mutex-lock-in-cpp3.png)
Multi-threading with a mutex lock is demonstrated in the above C++ code. Creating two threads allows for the repeated appending of each ID to the global vector g_data. The mutex g_data_mutex ensures that only one thread can access the session simultaneously.
The primary function combines the threads once they have finished their task, enabling safe access to g_data without a lock. The thread IDs are subsequently attached to the vector elements, which are then printed. The sequence of IDs may change due to concurrent execution, but each thread's ID always appears the predetermined Number of times (numIterations).
Example 4: Shows all the advantages of mutex lock:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex counterMutex; //Mutex to protect the shared counter
const int NUM_THREADS = 5;
//Number of threads
const int NUM_INCREMENTS = 10000; // Number of increments per thread
int sharedCounter = 0; // Shared counter variable
void incrementCounter()
{
for (int i = 0; i < NUM_INCREMENTS; ++i)
{
// Lock the Mutex to protect the critical section (incrementing the shared counter)
lock_guard<mutex> lock(counterMutex);
// Increment the shared counter safely
sharedCounter++;
}
}
int main()
{
std::thread threads[NUM_THREADS];
// Create threads to increment the shared counter concurrently
for (int threadNum = 0; threadNum < NUM_THREADS; ++threadNum)
{
threads[threadNum] = thread(incrementCounter);
}
// Wait for all threads to finish
for (int threadNum = 0; threadNum < NUM_THREADS; ++threadNum)
{
threads[threadNum].join();
}
// Output the final value of the shared counter
cout << "Final value of the shared counter: " << sharedCounter << endl;
return 0;
}
Output:
![Mutex Lock in C++](https://static.tutorialandexample.com/cpp/mutex-lock-in-cpp4.png)
Advantages of Mutex Lock
- Thread Safety: Mutex locks help to ensure that only one thread is given access to a shared resource or crucial area at a time. When multiple threads access the same shared data simultaneously, this avoids race situations and data corruption.
- Synchronization: Mutex locks allow threads to communicate with one another and coordinate their operations and order of execution through synchronization. Synchronization is essential when numerous threads must cooperate or do specific tasks in a particular order.
- Deadlock prevention: Mutex locks help prevent deadlocks, which happen when two or more threads become stuck while waiting for one another to release resources. By enforcing mutual exclusion, Mutex locks reduce the likelihood of deadlocks in multithreaded programs.
- Resource Protection: Mutex locks prevent concurrent access to shared resources, such as data structures, databases, or files. Mutex lock guarantees data consistency and integrity when used by several threads.
- Deterministic Behaviour: Mutex locks ensure predictable and orderly thread execution, preventing non-deterministic behavior resulting from unauthorized access to shared resources.
- Portability: Hardware and software widely support a feature of the C++11 standard known as mutex locks. Mutex lock-based programming is, therefore, more portable and easier to maintain.
- Granularity Control: Mutex locks allow programmers to regulate the level of synchronization.
Conclusion
Mutex locks in C++ guarantee thread safety and synchronization when multiple threads access the same shared resources. They safeguard crucial areas by avoiding data corruption and racial issues. Mutexes ensure atomicity by guaranteeing either the completion of the crucial part or none.
Mutexes optimize efficiency by cutting down on contention time by adding overhead. Critical sections with good design reduce bottlenecks. Performance can be improved by read-write locks when several threads want read access. Mutex locks are crucial in multithreaded systems' correctness, concurrency management, and synchronized access to shared resources.