Search code examples
c++multithreadingmutexcondition-variablespurious-wakeup

Thread safe queues and spurious wakes


I am currently reading a book about multi threading in C++. In one chapter I found some source code for a thread safe queue. It is roughly built like this:

template<typename T>
class QueueThreadSafe
{
private:
    std::mutex m_mutex;
    std::queue<T> m_dataQueue;
    std::condition_variable m_dataCondition;

public:
    void push(T someValue)
    {
        std::lock_guard<std::mutex> guard(m_mutex);
        m_dataQueue.push(someValue);
        m_dataCondition.notify_one();
    }

    void pop(T &retVal)
    {
        std::unique_lock<std::mutex> lock(m_mutex);
        m_dataCondition.wait(lock, [this]{return !m_dataQueue.empty();});
        retVal = m_dataQueue.front();
        m_dataQueue.pop();
    }
};

When a value is pushed to the queue, a data condition is notified and some (possible) waiting thread in pop can resume working. What confuses me are spurious wakes in this scenario. What if, at the same time one thread is notified, another thread just wakes up at the same time? Of course he also sees a not empty queue. In this scenario, two different threads would try to pop a value, where possibly only one value exists - a classical race condition.

Did I miss something here? Is there a better way to do this?


Solution

  • Spurious wakes just mean you need to check that the condition for the wake remains valid when you are woken. Since the wait function is passed:

    1. A lock, for mutual exclusion, and
    2. A predicate to determine if the wait has been satisfied

    the behavior when one thread is notified "normally", and another is notified spuriously is that one of them (doesn't matter which, whichever races faster) acquires the lock and confirms the queue is non-empty, then pops off the top element and releases the lock; the one that lost the race for the lock doesn't acquire the lock until the faster thread releases the lock, so it sees the already emptied queue and decides it was a spurious wakeup, going back to sleep.

    Importantly, it doesn't really matter whether the spuriously woken thread won the race for the lock (and the queued item) or not; one of the threads behaved as if woken normally (it found the condition true and worked as expected), one as if woken spuriously (it found the condition false and went back to waiting, as expected), and the code as a whole behaved correctly.