5

Suppose there are three threads A, B, and C. B and C suspend at a certain point, waiting for A to signal them to continue. Among the thread synchronization facilities provided by standard C++, std::condition_variable seems to best fit in here (though still bad). Since std::condition_variable must be used with a lock, the code for B and C may contain lines like:

{
  std::mutex mut;
  std::unique_lock<std::mutex> lock(mut);
  cond_var.wait(lock);  // cond_var is a global variable of type std::condition_variable`
}

Note that mut is used here not for synchronization purposes at all, but just to fit the signature of std::condition_variable::wait. With this observation, I'm thinking that maybe we can do better by implementing a dummy lock class, let's say dummy_lock, and replace std::condition_variable with std::condition_variable_any. dummy_lock meets the BasicLockable requirements with all its methods essentially doing nothing. Thereby, we get code similar to the following:

{
  dummy_lock lock;
  cond_var.wait(lock);  // cond_var is a global variable of type std::condition_variable_any`
}

This, if works at all, should be of higher efficiency than the original one. But the question is, does it even work according to the standard (language-lawyers are apt here)? Even if it works, this is by-no-means an elegant solution. So, do any of you folks have better ideas?

Lingxi
  • 14,579
  • 2
  • 37
  • 93
  • 3
    What if "A" signals B or C _before_ they suspend? How can you prove that will never happen, no matter how fast or slow the various threads execute? To answer the latter question, you will find that you actually do need a mutex. – Nemo Apr 03 '15 at 18:52
  • Your original code is UB because all threads waiting on a condition_variable must use the same mutex. condition_variable_any usually uses a plain cv and a mutex internally, so in all likelihood your second version is not saving anything over using a plain condition_variable. – T.C. Apr 03 '15 at 19:35
  • 1
    The `wait` function is not a conditional wait, it's an unconditional wait **for** a condition. You have no condition to wait for. So your code will only work by luck, if at all. If you had a condition to wait for, you'd need some way to atomically unprotect the condition and wait for the condition, which would require a real mutex. The whole point of a condition variable is provide an atomic "unlock and wait" primitive. – David Schwartz Apr 03 '15 at 21:01
  • @DavidSchwartz I get from your explanation the essence of condition variable and importance of the atomicity of unlock-wait. So, your argument is basically that I'm doing unconditionally wait in the example, and conditional variable does not fit in this case? – Lingxi Apr 04 '15 at 01:23
  • @Nemo I have to say you're right. I was thinking about Windows' Event. An Event has a state (signaled or non-signaled) and is different from a condition variable. It should be possible to implement Event using a condition variable, a Boolean state, and a mutex to guard the state. – Lingxi Apr 04 '15 at 01:28
  • 1
    @T.C. I agree with you that the modified version may not be any more efficient than the original one. With respect to the UB you mentioned, I personally think that it's based on the condition that the mutex is really used to guard something. When this condition is not true, whether the same mutex is used at all does not seem to matter. – Lingxi Apr 04 '15 at 01:42
  • @Lingxi Exactly. You need state somewhere, and condition variables are stateless. You don't implement the state. So you have a stateless, unconditional wait, which doesn't make sense. You can't wait for a bus unless you know something about where the bus is. – David Schwartz Apr 04 '15 at 18:02

2 Answers2

3

You are working on a false premise.

The mutex does not only protect the condition predicate, it also protects the condition_variable itself.

So the mutex should be at the same scope as the condition_variable and all locks should lock that same mutex.

like this:

// global scope
std::mutex mut;
std::condition_variable cond_var;

// thread scope
{
  std::unique_lock<std::mutex> lock(mut);
  cond_var.wait(lock);
}

see here: Why do pthreads’ condition variable functions require a mutex?

Community
  • 1
  • 1
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • 1
    From the comments to my question, I come to the idea that condition variable should be used with a mutex not because it has to, but because of the use case it is designed for. In other words, it is the use case that determines the use pattern of a conditional variable and mutex combination. Note that the saying *the wait will "atomically" unlock the mutex, allowing others access to the condition variable (for signalling)* (in the page you linked to) does not apply to `std::condition_variable`. See the notes in http://en.cppreference.com/w/cpp/thread/condition_variable/notify_all. – Lingxi Apr 04 '15 at 02:00
  • @richard-hodges I don't believe the claim that the mutext protects the condition_variable is correct. The standard does not appear to make any such a cliam. The original requirement of a mutex appears to come from pthreads as a design decision for performance reasons, but does not appear to make a claim that the mutext protects the condition variable: https://linux.die.net/man/3/pthread_cond_wait – Catskul Oct 25 '17 at 16:08
  • @Catskul Having read the linux docs again, I think your criticism is fair. It could be better worded. – Richard Hodges Oct 25 '17 at 16:54
  • It's unfortunately hard to find definitive information one way or the other : / I started reading the source of pthreads, but it's not easy to make a determination and even if a single implementation is safe, it doesn't mean all will be. – Catskul Oct 25 '17 at 17:00
0

What you're suggesting would be undefined behavior. The section on condition_variable_any leads with, emphasis mine:

A Lock type shall meet the BasicLockable requirements (30.2.5.2). [ Note: All of the standard mutex types meet this requirement. If a Lock type other than one of the standard mutex types or a unique_lock wrapper for a standard mutex type is used with condition_variable_any, the user must ensure that any necessary synchronization is in place with respect to the predicate associated with the condition_variable_any instance. —end note ]

The BasicLockable requirements themselves don't just describe the interface, they also describe the required semantics:

A type L meets the BasicLockable requirements if the following expressions are well-formed and have the specified semantics (m denotes a value of type L).

m.lock()
2 Effects: Blocks until a lock can be acquired for the current execution agent. If an exception is thrown then a lock shall not have been acquired for the current execution agent.

If your dummy_lock doesn't actually acquire a lock, it isn't a BasicLockable, and so you fail to meet the premise for condition_variable_any. At that point, all bets are off, and you cannot expect wait() to do anything reasonable.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 2
    I'm not using any predicate, so no synchronization is really necessary concerning that. I don't think `dummy_lock` does not meet the required semantics of `BasicLocable`. It's just that the satisfaction is trivial. – Lingxi Apr 04 '15 at 01:36