Home > OS >  Spurious wakeups only sometimes occur with std::condition_variable?
Spurious wakeups only sometimes occur with std::condition_variable?

Time:10-07

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.

  • Related