Does a condition variable really need another variable?

Viewed 275

Note: I'll give examples in C++ but I believe my question is language-agnostic. Correct me if I'm wrong.

Just so you really understand me - what I'm trying to learn here is what the tool does and nothing else. Not what it's usually used for, not what the conventions says, just what the blunt tool does. In this case - what the condition variable does.

So far it seems to me like it's a simple mechanism that allows threads to wait (block) until some other thread signals them (unblocks them). Nothing more, no dealing with critical section access or data access (of course they can be used for that but it's only a matter of programmer's choice). Also the signaling is usually only done when something important happens (e.g. data was loaded) but theoretically it could be called at any time. Correct so far?

Now, every example that I have seen uses a condition variable object (e.g. std::condition_variable) but also some additional variable to mark if something happened (e.g. bool dataWasLoaded). Take a look at this example from https://thispointer.com//c11-multithreading-part-7-condition-variables-explained/:

#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;
}

Now, other than the std::condition_variable m_condVar it also uses an additional variable bool m_bDataLoaded. But it seems to me that the thread performing mainTask is already notified that the data was loaded by means of std::condition_variable m_condVar. Why also check bool m_bDataLoaded for the same information? Compare (the same code without bool m_bDataLoaded):

#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;
public:
    Application()
    {
    }
    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);
        // Notify the condition variable
        m_condVar.notify_one();
    }
    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::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;
}
  1. Now I know about spurious wakeups and they alone necessitate the usage of an additional variable. My question is - are they they only reason for it? If they didn't occur could one just use condition variables without any additional variables (and btw wouldn't that make the name "condition variable" a misnomer then)?
  2. Another thing is - isn't the usage of additional variables the only reason why condition variables also require a mutex? If not, what are the other reasons?
  3. If additional variables are necessary (for spurious wakeups or other reasons) why doesn't the API require them (in the 2nd code I didn't have to use them for the code to compile)? (I don't know if it's the same in other languages, so this question might be C++-specific.)
2 Answers

It's not all about spurious wakeups.

When you call m_condvar.wait, how do you know the condition you're waiting for has not already happened?

Maybe 'loadData' has already been called in another thread. When it called notify_one(), nothing happened because there were no threads waiting.

Now if you call condvar.wait, you will wait forever because nothing will signal you.

The original version does not have this problem, because:

  1. If m_bDataLoaded is false, then it knows that the data is not loaded, and that after m_bDataLoaded is set true, the caller will signal the condition;
  2. The lock is held, and we know that m_bDataLoaded cannot be modified in another thread until it's released;
  3. condvar.wait will put the current thread in the waiting queue before releasing the lock, so we know that m_bDataLoaded will be set true after we start waiting, and so notify_one will also be called after we start waiting.

To answer your other questions:

  • Yes, coordination with additional variables is the reason why condition variables are tied to mutexes.
  • The API doesn't require, say, a boolean variable, because that's not always the kind of condition you're waiting for.

This kind of thing is common, for example:

Task *getTask() {
    //anyone who uses m_taskQueue or m_shutDown must lock this mutex
    unique_lock<mutex> lock(m_mutex);

    while (m_taskQueue.isEmpty()) {
        if (m_shutdown) {
            return null;
        }
        // this is signalled after a task is enqueued
        // or m_shutdown is asserted
        m_condvar.wait(lock);
    }
    return taskQueue.pop_front();
}

Here we require the same critical guarantee that the thread starts waiting before the lock is released, but the condition we're waiting for is more complex, involving a variable and separate data structure, and there are multiple ways to exit the wait.

Yes, the condition variable is just useful to wait for an event. In my point of view you should not try to use it for controlling concurrent access of critical data structures.

I just can speak about C++. As you see in the example here https://en.cppreference.com/w/cpp/thread/condition_variable/wait, they used this expression cv.wait(lk, []{return i == 1;});. And []{...} is the expression of a nameless function. So you can also write your own function and give the name of the function:

bool condFn()
{
    std::cout << "condFn" << std::endl; // debug output ;)
    return i == 1;
}

void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cerr << "Waiting... \n";
    cv.wait(lk, condFn);
    std::cerr << "...finished waiting. i == 1\n";
}

And inside this function you can evaluate, whatever you want. The thread is always sleeping until it gets notified, then it processes always the function that evaluates the condition for continue working. In case of true, the thread continues, in case of false the programm goes sleeping again.

Related