0

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?

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
Brotcrunsher
  • 1,964
  • 10
  • 32
  • 1
    @1201ProgramAlarm: It's [a known behavior of condition variables](https://en.wikipedia.org/wiki/Spurious_wakeup); the strategies used to optimize them render them vulnerable to accidental wakeup signals, and program logic needs to handle that by rechecking the condition after a wake and sleeping should the condition not be met. – ShadowRanger Oct 03 '17 at 02:00
  • 1
    Comment - some operating systems have a generic "atomic" function to wait for multiple objects, such as Windows [WaitForMultipleObjects()](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687025(v=vs.85).aspx). For a queue, the two objects could be a mutex and a semaphore. The semaphore "count" would reflect the number of elements in the queue. Race conditions and spurious wake ups are not an issue. – rcgldr Oct 03 '17 at 02:59
  • In the case of [WaitForMultipleObjects()](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687025(v=vs.85).aspx) used for a mutex and semaphore, in a single atomic call, the function waits for both the mutex and a non-zero semaphore "count", and it decrements the non-zero semaphore "count" before returning to the caller. This is part of what eliminates race conditions. – rcgldr Oct 03 '17 at 03:08

2 Answers2

2

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.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • Got it! The thing that confused me was the order of the wait operations. I thought that first the condition is checked, then the lock is aquired. But of course its vice versa. – Brotcrunsher Oct 03 '17 at 10:51
0

I think in that situation, the thread notified and the waked up thread have the same opportunity to pop from the queue, it only depends how the CPU make the schedule decision(which one is faster).

Unless you want specify which thread should have the right., then you have to change the implementation.