[...] you risk a thread unlocking the mutex, another thread signaling and then the original thread going to sleep and never waking up.
This scenario indeed occurs if the internal unlock
/wait
1 sequence is not atomic, as demonstrated in N2406. The solution, as outlined in the paper and also mentioned in your question, is straightforward: we could ensure that unlock
/wait
and notify operations (including notify_one
and notify_all
) are atomic with each other. This entails:
void notify_one(){
unique_lock<mutex> internal(mut);
cv.notify_one();
}
However, it appears that this solution is more encompassing than necessary. It suffices to ensure that unlock
/wait
is atomic with the notify operation, but the reverse is not essential, given that the internal notify operations are already atomic. To illustrate this point further, consider the following example2:
Thread A |
Thread B |
|
external.lock() |
|
Change predicate |
|
external.unlock() |
|
Waking Thread A up |
|
Enter condition_variable_any::notify_one |
|
unique_lock<mutex> internal(mut) |
external.lock() |
|
Check predicate, decide to wait3 |
|
Enter condition_variable_any::wait |
|
unique_lock<mutex> internal(mut) |
|
|
cv.notify_one() (1) |
|
internal.~unique_lock<mutex>() (2) |
external.unlock() |
|
cv.wait(internal) |
|
|
Exit condition_variable_any::notify_one |
... |
... |
In this example, the order of evaluation between (1) and (2) is inconsequential because even if A is awakened by B, it will interpret it as a spurious wakeup and continue waiting, leading to essentially the same outcome. Specifically:
Thread A |
Thread B |
... |
... |
|
unique_lock<mutex> internal(mut) |
external.lock() (3) |
|
Check predicate, decide to wait |
|
Enter condition_variable_any::wait |
|
unique_lock<mutex> internal(mut) |
|
|
internal.~unique_lock<mutex>() (2) |
external.unlock() |
|
cv.wait(internal) |
|
|
cv.notify_one() (1) |
Awakened by Thread B |
|
internal.~unique_lock<mutex>() |
|
Jump to (3) above |
|
... |
... |
As observed, releasing the mutex in notify functions prematurely poses the risk of a spurious wakeup. However, in practice, this has minimal impact as spurious wakeups are possible regardless. From a language-lawyer perspective, I believe libc++'s implementation is standard-conforming under the as-if rule. It's worth noting that in the above example, we attempt to notify after releasing the external lock. If we notify while holding the lock, there is minimal difference between libc++'s implementation and libstdc++'s implementation.
1Assuming that we use an internal condition_variable
to construct condition_variable_any
.
2Adapted and modified from N2406.
3The predicate may have been altered by another thread due to a race condition.
unlock_sleep()
. Can you provide a link to docs? Also, bynotify()
do you mean either ofnotify_one()
ornotify_all()
? – Argentite