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;
    queue dataQueue;

    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() and unlock(): 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.





Advertisement