3

Even if the shared variable is atomic, it must be modified under the mutex in order to correctly publish the modification to the waiting thread. Any thread that intends to wait on std::condition_variable has to acquire a std::unique_lock, on the same mutex as used to protect the shared variable

http://en.cppreference.com/w/cpp/thread/condition_variable

I understand that by protecting the std::condition_variable with a mutex we are protected against missing a notify if the waiting thread is not actually waiting. Answered here already: Shared atomic variable is not properly published if it is not modified under mutex

What I would like to know is if it's possible to use the mutex only to protect the std::condition_variable, and some other form of protection for the shared data? If we modify the example given in the other answer, would this work?

std::atomic_bool proceed(false);
std::mutex m;
std::condition_variable cv;

std::thread t([&m,&cv,&proceed]()
{
    {
        std::unique_lock<std::mutex> l(m);
        while(!proceed) {
            hardWork();
            cv.wait(l);
        }
    }
});

proceed = true;

{
    std::lock_guard<std::mutex> lock(m);
}
cv.notify_one();
t.join();

Or is there something going on with memory ordering or caches that I have missed?


Update


I'm aware that the mutex is normally protecting the shared data as well, using the atomic variable was just an example. The question is not about how to protect the shared data, but if it's necessary to use the same mutex to protect both. Another example using a second mutex:

bool proceed(false);
std::mutex boolMutex;

std::mutex cvMutex;
std::condition_variable cv;
std::unique_lock<std::mutex> l(cvMutex);

void setBool()
{
    std::lock_guard<std::mutex> lock(boolMutex);
    proceed = true;
}

bool checkBool()
{
    std::lock_guard<std::mutex> lock(boolMutex);
    return proceed;
}

void worker()
{
    while (true)
    {
        cv.wait(l);
        if (checkBool()) {
            // Do work
            return;
        }
    }
}

int main()
{
    std::thread t(worker);
    setBool();

    {
        std::lock_guard<std::mutex> lock(cvMutex);
    }
    cv.notify_one();
    t.join();

    return 0;
}
Community
  • 1
  • 1
Tim
  • 91
  • 2
  • 6

2 Answers2

0

The mutex-protected flag must be set, and the condition variable get signalled, while the mutex is still being held:

{
    std::lock_guard<std::mutex> lock(m);
    proceed = true;
    cv.notify_one();
}

Furthermore, in this case the proceed flag does not need to be an atomic entity. A simple

bool proceed;

will be sufficient. With access to proceed happening only while holding the associated mutex, making proceed atomic accomplishes absolutely nothing.

atomic entities are for handling exotic concurrency situations that do not involve any mutexes in the first place.

Sam Varshavchik
  • 84,126
  • 5
  • 57
  • 106
  • 1
    From what I've read previously, it's a good habit to unlock before notifying so that the waiting thread does not wake up to a locked mutex? And why must the flag be set while holding the mutex? If thread t is awake and inside hardWork(), the main thread will try and lock the mutex and simply wait until successful, then notify when thread t is back waiting on cv. – Tim Jul 04 '16 at 13:24
  • That is incorrect. All access to objects used by multiple threads must be protected by mutexes. This includes condition variables. – Sam Varshavchik Jul 04 '16 at 16:05
  • So the code example found here: http://en.cppreference.com/w/cpp/thread/condition_variable, the discussions here: http://stackoverflow.com/questions/35775501/c-should-condition-variable-be-notified-under-lock?lq=1 and here http://stackoverflow.com/questions/17101922/do-i-have-to-acquire-lock-before-calling-condition-variable-notify-one are incorrect? – Tim Jul 04 '16 at 17:06
  • @SamVarshavchik There's no need to have a locked mutex when you notify a condition variable. You should note that a condition variable is already linked to a mutex. From the man pages: The pthread_cond_broadcast() or pthread_cond_signal() functions may be called by a thread whether or not it currently owns the mutex that threads calling pthread_cond_wait() or pthread_cond_timedwait() have associated with the condition variable during their waits – Zan Lynx Jul 07 '16 at 21:56
  • Always read the docs, even if you *think* you know how an API works. Refresh your memory. – Zan Lynx Jul 07 '16 at 21:57
  • 1
    @ZanLynx Thank you for clarifying that. Do you have any insight into the original question as well? – Tim Jul 08 '16 at 11:36
  • @Tim It's the other way around. It's good habit to notify before unlocking because then the implementation knows that your notify can't possibly wake any threads and doesn't even have to check, allowing some expensive code to be avoided. The waiting thread won't wake up because a smart implementation knows that it needs the mutex to make forward progress. – David Schwartz Dec 03 '19 at 23:09
  • @SamVarshavchik I think this answer is not entirely correct. Could you possibly check my answer whether I didn't make some mistakes in my analysis? – Daniel Langr Apr 22 '20 at 14:35
0

I don't think Sam`s answer is correct. Consider the following code:

// thread #1:
std::unique_lock<std::mutex> l(m);
while (!proceed) cv.wait(l);

// thread #2:
proceed = true; // atomic to avoid data race
cv.notify_one();

The problem here is the following possible sequence of events:

thread #1: while (!proceed) // evaluated as true
thread #2: proceed = true;
thread #2: cv.notify_one();
thread #1: cv.wait(l); // never gets notified

To avoid this scenario, a typical solution is to protect modification of proceed with the same mutex:

// thread #1:
std::unique_lock<std::mutex> l(m);
while (!proceed) cv.wait(l);

// thread #2:
{
   std::lock_guard<std::mutex> l(m);
   proceed = true; // does not need to be atomic
}
cv.notify_one();

Now, proceed = true; must happen either before while (!proceed) or after cv.wait(l); starts waiting; both is ok. In the first case, there is no waiting at all; in the second case, cv.notify_one(); is guaranteed to happen only when cv.wait(l); is actually waiting.

Now, what about your (kind-of academic) case?

// thread #1:
std::unique_lock<std::mutex> l(m);
while (!proceed) cv.wait(l);

// thread #2:
proceed = true; // atomic to avoid data race
{
   std::lock_guard<std::mutex> lock(m);
}
cv.notify_one();

I believe this case is also perfectly valid, since the above-described wrong scenario cannot happen as well. For simple reason. If while (!proceed) is evaluated as false, again, there is no waiting. And, if while (!proceed) is evaluated as true, then notification cannot happen until cw.wait(l); is invoked.

Conclusion

I believe your first example is ok and the quote from cppreference is incorrect.

Daniel Langr
  • 18,256
  • 1
  • 39
  • 74