Thread Synchronization with Mutex : C++ Multithreading

Share the Article

Threads Race Condition

In order to understand Race condition lets take an example of multithreaded application . An application storing studentId of 5 students in queue. Job of the application is to print the studentID . For printing studentID we have created 2 threads printIdThread1 and printIdThread2. . Ideally we should get studentId from 1to 5 as output of below application. It is interesting to watch output of below application. This application has no mechanism for thread synchronization with mutex.

#include <iostream> // main header #include <thread> // for thread #include <queue> // for queue using namespace std; std::queue<int> studentId; void printId(int threadId) { if( studentId.empty()) { cout<<" No action required job already done"; return; } while(!studentId.empty()) { int a = studentId.front(); std::cout << "Thread Id " << threadId <<" Value " << a << endl; studentId.pop(); } } int main () { for(int i =1;i<=5;i++) studentId.push(i); // inserting studentId value into Queue std::thread printIdThread1 (printId,1); // 1st Thread std::thread printIdThread2 (printId,2); // 2nd Thread printIdThread1 .join(); //Main thread waiting for thread1 printIdThread2 .join(); //Main thread waiting for thread1 cout << "Main job done"<< endl; return 0; }

Both printIdThread1 and printIdThread2 tried to access global studentId at same time. Threads even tried to modified global studentId by popping out studentId from queue. For above application output is not fixed. It depends on race between the threads.

In Summary, Race condition occurred between 2 or more threads when they tried to access and modify the same resource at same time. As a results application may not perform correctly . Some time results are so much catastrophic that application may crash also .

C++ output of code using mutliple threads without synchronization with  any mutex

Various techniques of Threads Synchronization with Mutex

In above application we have seen issue with C++ multiple threads .In order to solve above problem we have various thread synchronization mechanism. From name it is clear we are trying to synchronizing threads so that output of application can be determined. No surprises and application will work as per user expectation.

In today’s article we will be mainly discussing about mutex and its types.

Mutex

Mutex is one of the best and easy technique for thread synchronization. Threads need to lock and unlock mutex while accessing the shared resources . For example, if we have 2 threads t1 and t2 and they want to access shared resource s1. Both t1 and t2 need to call mutex lock. Suppose, if t1 call mutex lock first so t2 need to wait on mutex lock until t1 call mutex unlock.

With synchronization, threads will not Race and corrupt shared resources. They will wait for their turn and ideally will move in sequential manner. So no corruption of global or shared data.

See below updated application with Mutex.

#include <iostream> // main header #include <thread> // for thread #include <queue> // for queue #include<mutex> // for mutex using namespace std; std::queue<int> studentId; std::mutex m1; void printId(int threadId) { m1.lock(); //Critical Section Starts while(!studentId.empty()) { int a = studentId.front(); std::cout << "Thread Id " << threadId <<" Value " <<a <<endl; studentId.pop(); } m1.unlock(); //Critical Section Ends } int main () { for(int i =1;i<=5;i++) studentId.push(i); // inserting value into Queue std::thread printIdThread1 (printId,1); // 1st Thread std::thread printIdThread2 (printId,2); // 2nd Thread printIdThread1.join(); //Main thread waiting for thread1 printIdThread2.join(); //Main thread waiting for thread1 cout<< "Main job done" << endl; return 0; }

In above example, one thread is locking the mutex and printing all the values.

Meanwhile other thread will also be blocked for its turn on mutex.

Other thread is only printing ” No action needed” as studentId is already printed.

C++ output of using mutex to perform thread synchronization with 2 threads

lock_guard

As we discussed mutex is definitely good option for doing thread synchronization and protect shared data. With mutex there is one drawback that it is must for thread to unlock the mutex .Otherwise ,other threads waiting for mutex will never be able to move further .They will get blocked for always.

Lets see an example below where function returns on empty queue and forgots to release mutex.

#include <iostream> // main header #include <thread> // for thread #include <queue> // for queue #include<mutex> // for mutex using namespace std; //for namespace std::queue<int> s1; std::mutex m1; void popQueue(int threadId) { m1.lock(); if( s1.empty()) { cout << "No action required job already done ThreadId " << threadId << endl; return; // forget to unlock mutex } while(!s1.empty()) { int a = s1.front(); std::cout << "Thread Id " << threadId <<" Value " <<a <<endl; s1.pop(); } m1.unlock(); } int main () { for(int i =1;i<=5;i++) s1.push(i); // instering value into Queue std::thread popThread1(popQueue,1); // 1st Thread std::thread popThread2(popQueue,2); // 2nd Thread std::thread popThread3(popQueue,3); // 3rd Thread cout<<"waiting for thread exit"<< endl; popThread1.join(); //Main thread waiting for thread1 cout<<"Thread 1 exit"<<endl; popThread2.join(); //Main thread waiting for thread2 cout<<"Thread 2 exit"<<endl; popThread3.join(); cout<<"Thread 3 exit"<<endl; cout<< "Main job done"<<endl; return 0; }

In above example if queue is empty ,thread will return after locking mutex . Other threads which are waiting for mutex lock will get blocked for ever.

C++ programs hangs indefinitely if any thread forgets to unlock the mutex

Solution for above problem is lock_guard.

Lock_guard will take care of mutex lock and unlock .

In lock_guard mutex is added as argument during construction. Therefore, if some exception occurred or user forgot to unlock mutex, local variable lock_guard will get destroyed automatically. On Destruction lock_gurad in turn releases mutex.

#include <iostream> // main header #include <thread> // for thread #include <queue> // for queue #include<mutex> // for mutex using namespace std; //for namespace std::queue<int> s1; std::mutex m1; void popQueue(int threadId) { //m1.lock(); std::lock_guard<std::mutex>lck(m1); if( s1.empty()) { cout<<"No action required job already done ThreadId " << threadId << endl; return; } while(!s1.empty()) { int a = s1.front(); std::cout << "Thread Id " << threadId << " Value " << a << endl; s1.pop(); } } int main () { for(int i =1;i<=5;i++) s1.push(i); // inserting value into Queue std::thread popThread1(popQueue,1); // 1st Thread std::thread popThread2(popQueue,2); // 2nd Thread std::thread popThread3(popQueue,3); // 2nd Thread cout<<"waiting for thread exit"<<endl; popThread1.join(); //Main thread waiting for thread1 cout<<"Thread 1 exit"<<endl; popThread2.join(); //Main thread waiting for thread2 cout<<"Thread 2 exit"<<endl; popThread3.join(); cout<<"Thread 3 exit"<<endl; cout<< "Main job done"<<endl; return 0; }

With lock_guard mutex now user do not have to bother about mutex lock unlock .Lock_gurad variable will take care.

C++ output of code which is using lock_guard with mutex

Recursive_Mutex for thread synchronization

Recursive_Mutex as it is clear from its name it is recursive in nature. Means, same thread can lock same mutex multiple times .

Lets take an example below where we are inserting and printing data .

#include <iostream> // main header #include <thread> // for thread #include <queue> // for queue #include<mutex> // for mutex using namespace std;//for namespace std::queue<int> s1; std::mutex m1; void popQueue() { std::lock_guard<std::mutex>lck(m1); //Second Lock if( s1.empty()) { cout << "No action required job already done ThreadId " << endl; return; } cout<<"Data started to popout"<<endl; while(!s1.empty()) { int a = s1.front(); std::cout << " Value "<<a<<endl; s1.pop(); } } void pushData() //Start here { std::lock_guard<std::mutex>lck(m1); //First Lock for(int i =1;i<=5;i++) s1.push(i); // inserting value into Queue cout<<"Data inserted"<<endl; popQueue(); } int main () { std::thread popThread1(pushData); // 1st Thread start cout<<"waiting for thread exit"<<endl; popThread1.join(); //Main thread waiting for thread1 cout<< "Main job done"<<endl; return 0; }

As, we have seen above after getting pop out no data is getting printed since same thread locked the mutex 2 times .Due to which it get blocked and finally full application is blocked.

C++ program hangs indefinitely when a thread locks same mutex 2 times

So solution for above problem is just to replace std::mutex with std::recuresive_mutex .

#include <iostream> // main header #include <thread> // for thread #include <queue> // for queue #include<mutex> // for mutex using namespace std;//for namespace std::queue<int> s1; std::recursive_mutex m1; void popQueue() { std::lock_guard<std::recursive_mutex>lck(m1); //Second Lock if( s1.empty()) { cout<<"No action required job already done ThreadId " << endl; return; } cout<<"Data started to popout"<<endl; while(!s1.empty()) { int a = s1.front(); std::cout << " Value "<<a<<endl; s1.pop(); } } void pushData() { std::lock_guard<std::recursive_mutex>lck(m1); //First Lock for(int i =1;i<=5;i++) s1.push(i); // inserting value into Queue cout<<"Data inserted"<<endl; popQueue(); } int main () { std::thread popThread1(pushData); // 1st Thread start cout<<"waiting for thread exit"<<endl; popThread1.join(); //Main thread waiting for thread1 cout<< "Main job done"<<endl; return 0; }

Output

C++ code demonstrating use of recursive_mutex for thread synchronization

Timed and Recursive_timed Mutex

Timed and recursive_timed mutex is mainly used for not waiting indefinitely for mutex.

As we have seen in above cases if mutex is not available thread will try to block indefinitely. But with timed mutex thead will only wait for mutex for specified time. Once that time elapses thread will comeout from blcoked state with proper return value try_lock_for () .

#include <iostream> // main header #include <thread> // for thread #include <queue> // for queue #include<mutex> // for mutex using namespace std;//for namespace std::queue<int> s1; std::timed_mutex m1; void popQueue2() { int d1 =m1.try_lock_for(std::chrono::seconds(5)); if(d1 ==0) { cout<<" Some issue in the code "<<endl; return; } cout<<"Data started to popout"<<endl; while(!s1.empty()) { int a = s1.front(); std::cout << " Value "<<a<<endl; s1.pop(); } } void popQueue1() { //std::lock_guard<std::timed_mutex>lck1(m1); m1.try_lock_for(std::chrono::seconds(5)); while(!s1.empty()) { int a = s1.front(); std::cout << " Value "<<a<<endl; s1.pop(); } std::this_thread::sleep_for(std::chrono::seconds(10)); m1.unlock(); } int main () { for(int i =1;i<=5;i++) s1.push(i); // inserting value std::thread popThread1(popQueue1); // 1st Thread std::thread popThread2(popQueue2); // 1st Thread cout<<"waiting for thread exit"<<endl; popThread1.join(); //Main thread waiting for thread1 popThread2.join(); cout<< "Main job done"<<endl; return 0; }

Output

C++ code demonstrating timed_mutex

Main Funda: There are multiple types of mutex and those are very important to avoid race condition between threads.

Related Topics:

What is a Tuple, a Pairs and a Tie in C++
C++ Multithreading: Understanding Threads
What is Copy Elision, RVO & NRVO?
Lambda in C++11
Lambda in C++17
What are the drawbacks of using enum ?
Class Template Argument Deduction in C++17
How to stop compiler from generating special member functions?
Compiler Generated Destructor is always non-virtual
How to make a class object un-copyable?
Why virtual functions should not be called in constructor & destructor ?
How std::forward( ) works?
Rule of Three
How std::move() function works?
What is reference collapsing?
How delete keyword can be used to filter polymorphism
emplace_back vs push_back

Share the Article

Leave a Reply

Your email address will not be published. Required fields are marked *