Why there is no wait function for condition_variable that does not block the mutex

Consider the following example.

std::mutex mtx; std::condition_variable cv; void f() { { std::unique_lock<std::mutex> lock( mtx ); cv.wait( lock ); // 1 } std::cout << "f()\n"; } void g() { std::this_thread::sleep_for( 1s ); cv.notify_one(); } int main() { std::thread t1{ f }; std::thread t2{ g }; t2.join(); t1.join(); } 

g() “knows” that f() waiting in a script that I would like to discuss. According to cppreference.com there is no need for g() block the mutex before calling notify_one . Now, in the line with the inscription "1" cv , the mutex will be released and it will be deleted after sending the notification. The lock destructor releases it again immediately afterwards. This seems redundant, especially because locking is expensive. (I know that in some scenarios the mutex should be blocked. But this is not the case here.)

Why does condition_variable not have a wait_nolock function that does not delete the mutex after receiving a notification. If the answer is that pthreads do not provide such functionality: why can't pthreads be extended to provide it? Is there an alternative to implementing the desired behavior?

+5
source share
1 answer

You misunderstand what your code does.

Your code in line // 1 free not to block at all. condition_variables can (and will!) have side awakenings - they can wake up without a good reason.

You are responsible for verifying whether the awakening is false.

There are 3 things required with condition_variable :

  • A condition_variable
  • A mutex
  • Some data protected by mutex

mutex protected data is modified (under mutex ). Then (with mutex possibly disabled), condition_variable notified.

At the other end, you will block mutex , and then wait for the condition variable. When you wake up, your mutex will be blocked, and you will check if the wakeup is false by looking at the data protected by mutex . If this is a valid awakening, you process and continue.

If this is an invalid awakening, you return to waiting.

In your case, you do not have protected data, you cannot distinguish false awakenings from real ones, and your design is incomplete.

It is not surprising that with an incomplete design, you don’t see the reason mutex restarts: it is blocked, so you can safely check the data to make sure that the awakening was false or not.

If you want to know why condition variables are designed in this way, probably because this project is more efficient than “reliable” (for some reason), and instead of exposing higher-level primitives, C ++ showed more low level more efficient primitives.

Creating a higher level abstraction on top of this is not difficult, but there are design solutions. Here is one std::experimental::optional built on top:

 template<class T> struct data_passer { std::experimental::optional<T> data; bool abort_flag = false; std::mutex guard; std::condition_variable signal; void send( T t ) { { std::unique_lock<std::mutex> _(guard); data = std::move(t); } signal.notify_one(); } void abort() { { std::unique_lock<std::mutex> _(guard); abort_flag = true; } signal.notify_all(); } std::experimental::optional<T> get() { std::unique_lock<std::mutex> _(guard); signal.wait( _, [this]()->bool{ return data || abort_flag; }); if (abort_flag) return {}; T retval = std::move(*data); data = {}; return retval; } }; 

Now each send can lead to success of get on the other end. If more than one send is encountered, only one of them is consumed by get . If and when abort_flag set, instead of get() , {} returned immediately;

The above is supported by several consumers and manufacturers.

An example of the use of the above can be a preview state source (for example, a user interface stream) and one or more rendering renderers (which are not fast enough to run in a user interface stream).

Preview state uploads the preview state to data_passer<preview_state> willy-nilly. Players compete, and one of them captures it. Then they display it and pass it back (through some mechanism).

If the preview states arrive faster than the renderers consume them, only the last of them is interesting, so the previous ones are discarded. But existing previews are not interrupted just because a new state appears.


Questions that are asked below the conditions of the race.

If the transmitted data is atomic , we can not do without a mutex on the sending side?

So something like this:

 template<class T> struct data_passer { std::atomic<std::experimental::optional<T>> data; std::atomic<bool> abort_flag = false; std::mutex guard; std::condition_variable signal; void send( T t ) { data = std::move(t); // 1a signal.notify_one(); // 1b } void abort() { abort_flag = true; // 1a signal.notify_all(); // 1b } std::experimental::optional<T> get() { std::unique_lock<std::mutex> _(guard); // 2a signal.wait( _, [this]()->bool{ // 2b return data.load() || abort_flag.load(); // 2c }); if (abort_flag.load()) return {}; T retval = std::move(*data.load()); // data = std::experimental::nullopt; // doesn't make sense return retval; } }; 

the above does not work.

Let's start by listening. He performs step 2a, then waits (2b). It evaluates the condition in step 2c, but has not yet returned from lambda.

The broadcast stream then performs step 1a (data setting), then signals a condition variable. At the moment, no one is waiting for a condition variable (the code in lambda is not counted!).

Then the listening thread ends the lambda and returns a false wake. Then it locks the condition variable and never notices that the data has been sent.

std::mutex , used when waiting for a condition variable, should protect the write to the data "passed" by the condition variable (no matter what test you do to determine if the wakeup is false) and read (in lambda), or there is a possibility "lost signals." (At least in a simple implementation: more complex implementations can create loose paths for “ordinary cases” and use only mutex in double checking. This is beyond the scope of this question.)

The use of atomic variables does not get around this problem, because the two operations “determine whether the message was false” and “overwrite in the condition variable” should be atomic with respect to the “falsity” of the message.

+13
source

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


All Articles