I'm trying to demonstrate the spurious wakeup behavior of std::condition_variable
using a simple program compiled with MSVC (version 19.32.31332, if that matters). However, I have begun to notice that spurious wakeups only occur when the two-argument overload of the wait
method is used. This appears to be true in both Debug and Release modes.
I can easily detect spurious wakeups with the Predicate
argument of the two-argument overload:
Code
#include <iostream>
#include <condition_variable>
#include <future>
#include <mutex>
#include <random>
#include <thread>
#include <vector>
using namespace std::chrono_literals;
#define NUM_CVS 6
#define NUM_THREADS 12
std::mutex cout_mutex;
std::mutex m[NUM_CVS];
std::condition_variable cv[NUM_CVS];
bool data[NUM_CVS];
bool check_data(std::size_t i) {
bool d = data[i];
{
std::lock_guard<std::mutex> lg(cout_mutex);
std::cout << std::this_thread::get_id() << ": Woken... " << (d ? "Waited." : "Spurious.") << std::endl;
}
return d;
}
void task(std::size_t i) {
{
std::lock_guard<std::mutex> lg(cout_mutex);
std::cout << std::this_thread::get_id() << ": Locking..." << std::endl;
}
std::unique_lock<std::mutex> l(m[i]);
{
std::lock_guard<std::mutex> lg(cout_mutex);
std::cout << std::this_thread::get_id() << ": Locked. Waiting..." << std::endl;
}
cv[i].wait(l, [&] {return check_data(i); });
}
int main()
{
std::random_device rd;
std::default_random_engine dre(rd());
std::uniform_int_distribution<std::size_t> d(0, NUM_CVS - 1);
std::vector<std::future<void>> v;
for (auto i = 0; i < NUM_THREADS; i) {
v.push_back(std::async(task, d(dre)));
}
std::this_thread::sleep_for(1000ms);
for (auto i = 0; i < NUM_CVS; i) {
auto index = i;
{
std::lock_guard<std::mutex> g(m[index]);
data[index] = true;
}
cv[index].notify_one();
}
for (auto& f : v) {
f.wait();
}
}
Console Output
25220: Locking...
22672: Locking...
22672: Locked. Waiting...
22672: Woken... Spurious.
26224: Locking...
26224: Locked. Waiting...
26224: Woken... Spurious.
22356: Locking...
22356: Locked. Waiting...
22356: Woken... Spurious.
25220: Locked. Waiting...
25220: Woken... Spurious.
18304: Locking...
18304: Locked. Waiting...
18304: Woken... Spurious.
26544: Locking...
21132: Locking...
21132: Locked. Waiting...
21132: Woken... Spurious.
19144: Locking...
19144: Locked. Waiting...
19144: Woken... Spurious.
21584: Locking...
21584: Locked. Waiting...
21584: Woken... Spurious.
26544: Locked. Waiting...
26544: Woken... Spurious.
25872: Locking...
25872: Locked. Waiting...
25872: Woken... Spurious.
24928: Locking...
3956: Locking...
3956: Locked. Waiting...
3956: Woken... Spurious.
24928: Locked. Waiting...
24928: Woken... Spurious.
22356: Woken... Waited.
22672: Woken... Waited.
21584: Woken... Waited.
25220: Woken... Waited.
21132: Woken... Waited.
26224: Woken... Waited.
However, when I don't provide a predicate and simply check for spurious wakeups after the fact, the program behaves as though spurious wakeups are no longer possible:
Code
void task(std::size_t i) {
{
std::lock_guard<std::mutex> lg(cout_mutex);
std::cout << std::this_thread::get_id() << ": Locking..." << std::endl;
}
std::unique_lock<std::mutex> l(m[i]);
{
std::lock_guard<std::mutex> lg(cout_mutex);
std::cout << std::this_thread::get_id() << ": Locked. Waiting..." << std::endl;
}
cv[i].wait(l);
check_data(i);
}
Output
23592: Locking...
23592: Locked. Waiting...
20388: Locking...
20388: Locked. Waiting...
26536: Locking...
26536: Locked. Waiting...
18436: Locking...
18436: Locked. Waiting...
1528: Locking...
1528: Locked. Waiting...
21236: Locking...
21236: Locked. Waiting...
11784: Locking...
11784: Locked. Waiting...
21776: Locking...
21776: Locked. Waiting...
20796: Locking...
20796: Locked. Waiting...
21472: Locking...
21472: Locked. Waiting...
23988: Locking...
23988: Locked. Waiting...
24988: Locking...
24988: Locked. Waiting...
20388: Woken... Waited.
23592: Woken... Waited.
21236: Woken... Waited.
26536: Woken... Waited.
21472: Woken... Waited.
11784: Woken... Waited.
Is thisan unusual implementation detail of MSVC, the intended behavior of std::condition_variable
, or simply a bug?
P.S. I am aware the main thread may emit notifications before the task threads wait on them. I considered the 1 second delay sufficient mitigation for this example.
CodePudding user response:
None of the "spurious wakeups" you see in the first example are actually spurious wakeups. If you look at wait the example implementation clearly shows that the predicate is executed before actually blocking on the CV. There are exactly 12 spurious wakeups in the log - for your 12 threads. Each thread checks the predicate first, before actually blocking.
Your detection logic for when a spurious wakeup occurred has false positives.