Possible race condition in std :: condition_variable?

I reviewed the implementation of VC ++ std::condition_variable(lock,pred) , basically, it looks like this:

 template<class _Predicate> void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) { // wait for signal and test predicate while (!_Pred()) wait(_Lck); } 

Basically, a bare wait calls _Cnd_waitX , which calls _Cnd_wait , which calls do_wait , which calls cond->_get_cv()->wait(cs); (they are all in the cond.c file).

cond->_get_cv() returns Concurrency::details::stl_condition_variable_interface .

If we go to the primitives.h file, we will see that under windows 7 and above we have the stl_condition_variable_win7 class, which contains the good old win32 CONDITION_VARIABLE and wait calls __crtSleepConditionVariableSRW .

When doing a little debugging of the assembly, __crtSleepConditionVariableSRW just extract the pointer to the SleepConditionVariableSRW function and name it.

Here's what: as far as I know, win32 CONDITION_VARIABLE not a kernel object, but a user object. Therefore, if any thread notifies this variable and the thread does not actually sleep on it, you have lost the notification, and the thread will remain asleep until it reaches the timeout, or some other thread will notify about it. A small program can prove this - if you skip the alert point, your thread will stay awake, although some other thread has notified it.

My question is this:
one thread expects a condition variable, and the predicate returns false. Then comes the whole chain of calls described above. During this time, another thread changed the environment, so the predicate will return true and notify the condition variable. We passed the predicate in the source stream, but we still did not get into SleepConditionVariableSRW - the call chain is very long.

So, although we notify the condition variable and a predicate placed in the condition variable will definitely return true (since the notifier did this), we still block the condition variable, possibly forever.

How should this behave? A big ugly race condition seems to be waiting. If you notify the condition variable, and the predicate returns true, the stream must be unlocked. But if we are in the uncertainty between predicate checking and sleep, we are locked forever. std::condition_variable::wait not an atomic function.

What does the standard say about this, and is it really a race condition?

+5
source share
1 answer

You have violated the contract, so all bets are disabled. See: http://en.cppreference.com/w/cpp/thread/condition_variable

TL; DR: It is not possible for a predicate to be modified by someone else while you are holding a mutex.

You must change the predicate base variable while holding the mutexes and you need to acquire this mutex before calling std::condition_variable::wait (both because wait releases the mutex, but because the contract).

In the scenario you described, the change occurred after while (!_Pred()) saw that the predicate was not held, but before wait(_Lck) was able to free the mutex. This means that you changed what the predicate checks without holding the mutex. You have violated the rules, race conditions or endless waiting - these are still not the worst types of UB you can get. At least they are local and related to the rules that you broke so that you can find the error ...

If you play by the rules, either:

  • Waiter takes mutex first
  • Included in std::condition_variable::wait . (Recall that the notifier is still waiting for the mutex.)
  • Checks the predicate and sees that it is not held. (Recall that the notifier is still waiting for the mutex.)
  • Call some implementation of a certain magic to free the mutex and wait, and only now can there be a notifier.
  • The notification finally managed to accept the mutex.
  • The detector changes everything that needs to be changed in order for the predicate to hold true.
  • The notifier calls std::condition_variable::notify_one .

or

  • the notifier receives the mutex. (Recall that the waiter is locked while trying to get a mutex.)
  • The notification changes everything that needs to be changed in order for the predicate to hold true. (Recall that the waiter is still locked.)
  • The notification releases the mutex. (Somewhere along the way, the waiter will call std::condition_variable::notify_one , but as soon as the mutex is released ...)
  • The waiter receives a mutex.
  • The waiter calls std::condition_variable::wait .
  • The waiter checks while (!_Pred()) and alt! the predicate is true.
  • The waiter does not even enter the internal wait , so regardless of whether the notifier managed to call std::condition_variable::notify_one or failed, it does not matter.

This is the rationale for the requirement at cppreference.com:

Even if the shared variable is atomic, it must be changed under the mutex in order to correctly publish the modification in the waiting thread.

Note that this is a general rule for condition variables, not special requirements for std::condition_variables (including Windows CONDITION_VARIABLE s, POSIX pthread_cond_t s, etc.).


Recall that the wait overload, which takes a predicate, is just a convenient function, so the caller does not need to deal with false awakenings. The standard (Β§30.5.1 / 15) explicitly states that this overload is equivalent to a while loop in a Microsoft implementation:

Effects: Equivalent:

 while (!pred()) wait(lock); 

Does simple wait ? Do you check the predicate before and after calling wait ? Excellent. You do the same. Or you also ask void std::condition_variable::wait( std::unique_lock<std::mutex>& lock ); ?


Critical Windows and Slim Reader / Writer sections Locks, which are user-mode objects and not kernel objects, are irrelevant and irrelevant. There are alternative implementations. If you are interested in learning how Windows can atomically release CS / SRWL and enter a wait state (which are naive user-mode implementations before Vista, with Mutexes and Events errors), that’s another question.

+2
source

Source: https://habr.com/ru/post/1264742/


All Articles