Mutex and Condition Variables in C++
In C++, multithreading enables concurrent execution of code, but when multiple threads access shared resources, proper synchronization is necessary to avoid data races and ensure correct execution. This is where std::mutex
and std::condition_variable
come into play. These tools help manage access to shared resources and allow threads to communicate safely with each other.
What is a Mutex?
A std::mutex
is a synchronization primitive used to protect shared data from being simultaneously accessed by multiple threads. It ensures that only one thread can access the critical section of code at a time, preventing race conditions.
Using std::mutex
for Locking
To use a mutex, you lock it before accessing shared resources and unlock it once the access is complete. If another thread tries to lock the same mutex, it will be blocked until the mutex is unlocked.
Example:
#include <iostream> #include <thread> #include <mutex> using namespace std; mutex mtx; // Declare a mutex void printMessage(int n) { mtx.lock(); // Lock the mutex cout << "Thread " << n << " is printing safely!" << endl; mtx.unlock(); // Unlock the mutex } int main() { thread t1(printMessage, 1); thread t2(printMessage, 2); t1.join(); // Wait for thread t1 to finish t2.join(); // Wait for thread t2 to finish return 0; }
In this example, the mutex mtx
is used to ensure that only one thread can print a message at a time. The second thread will be blocked until the first thread unlocks the mutex.
What is a Condition Variable?
A std::condition_variable
is a synchronization primitive that allows threads to wait for certain conditions to be met before proceeding. It is often used in scenarios where one thread needs to wait for another thread to signal that a task has been completed or a certain condition has been reached.
Condition variables are typically used with a mutex to ensure that the condition check and the waiting process are done atomically.
Using std::condition_variable
for Thread Synchronization
The basic idea behind a condition variable is that a thread can wait for a certain condition to become true. Other threads can notify waiting threads when the condition is met, allowing them to proceed.
Example:
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> using namespace std; mutex mtx; condition_variable cv; bool ready = false; // Shared condition variable void printMessage(int n) { unique_lock<mutex> lock(mtx); // Lock the mutex cv.wait(lock, []{ return ready; }); // Wait for 'ready' to be true cout << "Thread " << n << " is printing safely!" << endl; } void notifier() { this_thread::sleep_for(chrono::seconds(1)); { lock_guard<mutex> lock(mtx); // Lock the mutex ready = true; // Set the condition to true } cv.notify_all(); // Notify all waiting threads } int main() { thread t1(printMessage, 1); thread t2(printMessage, 2); thread t3(notifier); // This thread notifies other threads t1.join(); t2.join(); t3.join(); return 0; }
In this example, threads t1
and t2
are waiting for the ready
flag to become true. The notifier
thread changes the value of ready
after a brief delay and notifies both threads to proceed.
How Mutex and Condition Variables Work Together
Mutexes and condition variables often work together to synchronize threads. Typically, a thread locks a mutex, checks or changes a condition, and then either waits for a condition variable or notifies other threads.
The wait()
function of a condition variable is used by a thread to wait until another thread calls notify_one()
or notify_all()
. When a thread calls wait()
, it releases the mutex and puts itself in a waiting state. Once the condition is satisfied, it acquires the mutex again and continues execution.
Example: Producer-Consumer Problem
In the producer-consumer problem, one or more threads (producers) produce data that is consumed by other threads (consumers). A condition variable is used to synchronize the producers and consumers, ensuring that consumers wait for data to be produced and producers wait when the buffer is full.
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue> using namespace std; mutex mtx; condition_variable cv; queuedataQueue; void producer() { for (int i = 0; i < 5; i++) { { lock_guard<mutex> lock(mtx); // Lock the mutex dataQueue.push(i); // Produce data cout << "Produced: " << i << endl; } cv.notify_all(); // Notify consumers this_thread::sleep_for(chrono::seconds(1)); } } void consumer() { while (true) { unique_lock<mutex> lock(mtx); // Lock the mutex cv.wait(lock, []{ return !dataQueue.empty(); }); // Wait until data is available int data = dataQueue.front(); dataQueue.pop(); // Consume data cout << "Consumed: " << data << endl; this_thread::sleep_for(chrono::seconds(1)); } } int main() { thread t1(producer); thread t2(consumer); t1.join(); t2.join(); return 0; }
In this example, the producer thread generates data and pushes it into the dataQueue
. The consumer thread waits for data to be available, and once the producer notifies it, the consumer consumes the data.
Key Methods for Mutexes and Condition Variables
lock()
andunlock()
: Used to manually lock and unlock a mutex.wait()
: Used by a thread to wait for a condition variable to be notified. It releases the mutex and blocks the thread until it is notified.notify_one()
: Notifies one thread that is waiting on the condition variable.notify_all()
: Notifies all threads that are waiting on the condition variable.
Conclusion
In C++, mutexes and condition variables are powerful tools for synchronizing threads and ensuring thread safety. By using std::mutex
to protect shared data and std::condition_variable
to synchronize actions between threads, developers can create safe and efficient multithreaded programs.