Please explain the use of condition variables in c++ threads, and why do we need to use `unique_lock` and `mutex` alongwith this

10,363

Condition variables allow one to atomically release a held mutex and put the thread to sleep. Then, after being signaled, atomically re-acquire the mutex and wake up. You run into this, for example, in the producer/consumer problem. You will deadlock if you go to sleep while holding the mutex, but you could also deadlock if you release it before sleeping (by missing the signal to wake up).

It's not something that can be explained in a few paragraphs without examples, and there are several well-known pitfalls and caveats to using condition variables. Check out "An Introduction to Programming with Threads" by Andrew D. Birrell.

Regardless of the language, condition variables always take a mutex. The mutex must be held when wait is called. You should always verify that the desired condition is still true after returning from wait. That's why you always see conditional waits wrapped in a while loop. C++11 also gives you the predicate overload, which is syntactic sugar for the while loop.

The mutex protects the shared state. The condition lets you block until signaled.

unique_lock is an RAII (Resource Acquisition Is Initialization) wrapper for locking and unlocking the given mutex. It's conceptually identical to the lock statement in C#. It simplifies exception handling by tying the mutex acquisition and release to the lifetime of the unique_lock instance. I don't know if there's a reason why condition_variable forces you to use it other than the fact that it's good practice. The only difference between unique_lock and lock_guard is that unique_lock can be unlocked... which is why you have to use it instead of lock_guard with condition_variable.

Share:
10,363
Nilesh Kumar
Author by

Nilesh Kumar

I'm just regular, everyday, normal developer

Updated on June 08, 2022

Comments

  • Nilesh Kumar
    Nilesh Kumar almost 2 years

    I am refering to this particular piece of code:

    this code basically has three threads 1. Perform some handshaking with server 2. Load Data from XML files. 3. Do processing on data loaded from XML. As we can see that Task 1 is not dependent on any other Tasks but Task 3 is dependent on Task 2. So, it means Task 1 and Task 2 can be run in parallel by different Threads to improve the performance of application. Thus, application is built to be multithreaded.

    #include <iostream>
    #include <thread>
    #include <functional>
    #include <mutex>
    #include <condition_variable>
    using namespace std::placeholders;
    
    class Application
    {
      std::mutex m_mutex;
      std::condition_variable m_condVar;
      bool m_bDataLoaded;
    public:
      Application()
      {
        m_bDataLoaded = false;
      }
      void loadData()
      {
       // Make This Thread sleep for 1 Second
       std::this_thread::sleep_for(std::chrono::milliseconds(1000));
       std::cout<<"Loading Data from XML"<<std::endl;
       // Lock The Data structure
       std::lock_guard<std::mutex> guard(m_mutex);
       // Set the flag to true, means data is loaded
       m_bDataLoaded = true;
       // Notify the condition variable
       m_condVar.notify_one();
      }
      bool isDataLoaded()
      {
        return m_bDataLoaded;
      }
      void mainTask()
      {
        std::cout<<"Do Some Handshaking"<<std::endl;
        // Acquire the lock
        std::unique_lock<std::mutex> mlock(m_mutex);
        // Start waiting for the Condition Variable to get signaled
        // Wait() will internally release the lock and make the thread to block
        // As soon as condition variable get signaled, resume the thread and
        // again acquire the lock. Then check if condition is met or not
        // If condition is met then continue else again go in wait.
        m_condVar.wait(mlock, std::bind(&Application::isDataLoaded, this));
        std::cout<<"Do Processing On loaded Data"<<std::endl;
      }
    };
    int main()
    {
       Application app;
       std::thread thread_1(&Application::mainTask, &app);
       std::thread thread_2(&Application::loadData, &app);
       thread_2.join();
       thread_1.join();
       return 0;
    }
    

    this code is from http://thispointer.com/c11-multithreading-part-7-condition-variables-explained/

    Thanks