5

I have a wrapper around std::queue using C++11 semantics to allow concurrent access. The std::queue is protected with a std::mutex. When an item is pushed to the queue, a std::condition_variable is notified with a call to notify_one.

There are two methods for popping an item from the queue. One method will block indefinitely until an item has been pushed on the queue, using std::condition_variable::wait(). The second will block for an amount of time given by a std::chrono::duration unit using std::condition_variable::wait_for():

template <typename T> template <typename Rep, typename Period>
void ConcurrentQueue<T>::Pop(T &item, std::chrono::duration<Rep, Period> waitTime)
{
    std::cv_status cvStatus = std::cv_status::no_timeout;
    std::unique_lock<std::mutex> lock(m_queueMutex);

    while (m_queue.empty() && (cvStatus == std::cv_status::no_timeout))
    {
        cvStatus = m_pushCondition.wait_for(lock, waitTime);
    }

    if (cvStatus == std::cv_status::no_timeout)
    {
        item = std::move(m_queue.front());
        m_queue.pop();
    }
}

When I call this method like this on an empty queue:

ConcurrentQueue<int> intQueue;

int value = 0;
std::chrono::seconds waitTime(12);

intQueue.Pop(value, waitTime);

Then 12 seconds later, the call to Pop() will exit. But if waitTime is instead set to std::chrono::seconds::max(), then the call to Pop() will exit immediately. The same occurs for milliseconds::max() and hours::max(). But, days::max() works as expected (doesn't exit immediately).

What causes seconds::max() to exit right away?

This is compiled with mingw64:

g++ --version

g++ (rev5, Built by MinGW-W64 project) 4.8.1
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Tim Flynn
  • 53
  • 1
  • 3

2 Answers2

4

To begin with, the timed wait should likely be a wait_until(lock, std::chrono::steady_clock::now() + waitTime);, not wait_for because the loop will now simply repeat the wait multiple times until finally the condition (m_queue.empty()) becomes true. The repeats can also be caused by spurious wake-ups.

Fix that part of the code by using the predicated wait methods:

template <typename Rep, typename Period>
bool pop(std::chrono::duration<Rep, Period> waitTime, int& popped)
{
    std::unique_lock<std::mutex> lock(m_queueMutex);

    if (m_pushCondition.wait_for(lock, waitTime, [] { return !m_queue.empty(); }))
    {
        popped = m_queue.back();
        m_queue.pop_back();
        return true;
    } else
    {
        return false;
    }
}

On my implementation at least seconds::max() yields 0x7fffffffffffffff

§30.5.1 ad 26 states:

Effects: as if

 return wait_until(lock, chrono::steady_clock::now() + rel_time);

Doing

auto time = steady_clock::now() + seconds::max();
std::cout << std::dec << duration_cast<seconds>(time.time_since_epoch()).count() << "\n";

On my system, prints

265521

Using date --date='@265521' --rfc-822 told me that that is Sun, 04 Jan 1970 02:45:21 +0100

There's a wrap around bug going on for GCC and Clang, see below


Tester

Live On Coliru

#include <thread>
#include <condition_variable>
#include <iostream>
#include <deque>
#include <chrono>
#include <iomanip>

std::mutex m_queueMutex;
std::condition_variable m_pushCondition;

std::deque<int> m_queue;

template <typename Rep, typename Period>
bool pop(std::chrono::duration<Rep, Period> waitTime, int& popped)
{
    std::unique_lock<std::mutex> lock(m_queueMutex);

    if (m_pushCondition.wait_for(lock, waitTime, [] { return !m_queue.empty(); }))
    {
        popped = m_queue.back();
        m_queue.pop_back();
        return true;
    } else
    {
        return false;
    }
}

int main()
{
    int data;
    using namespace std::chrono;

    pop(seconds(2)    , data);

    std::cout << std::hex << std::showbase << seconds::max().count() << "\n";
    auto time = steady_clock::now() + seconds::max();
    std::cout << std::dec << duration_cast<seconds>(time.time_since_epoch()).count() << "\n";
    pop(seconds::max(), data);
}
sehe
  • 374,641
  • 47
  • 450
  • 633
  • I'll try out now() + rel_time, thanks. But: "because the loop will now simply repeat the wait multiple times until finally the condition (m_queue.empty()) becomes true" Not sure I agree with that because the loop will exit when the wait_for() times out since cvStatus will no longer be equal to no_timeout. – Tim Flynn Dec 31 '14 at 23:05
  • 1
    No difference using wait_until vs. wait_for with a predicate vs. my original wait_for method...seconds::max() exits immediately in both cases. This wrap around bug is certainly annoying. – Tim Flynn Dec 31 '14 at 23:23
  • @TimFlynn I meant this: [this `Pop()` with `waitTime` of 2 seconds wait time blocks infinitely because of the repeated timed waits _with new relative deadlines_](http://coliru.stacked-crooked.com/a/51e90fe3f502c05d). This is a bug, unless you don't actually want the timeout to take effect. (Excuse the dealy, it's just past new year here :)) – sehe Dec 31 '14 at 23:48
  • @TimFlynn re: "... in both cases". Obviously: the quoted standard **specificies** they must do the same! I only used the quivalence so I could show that it is taking a deadline of ~`04 Jan 1970`. In addition, you could use **[`wait_until` without predicate to combat your infite loop behaviour](http://coliru.stacked-crooked.com/a/0fb0f133a7823e95)**. Note that this is just a tedious, verbose way to write the predicated call from my answer, though. – sehe Dec 31 '14 at 23:54
  • 1
    Ah ok I see! Thanks for explaining that again, I misread/misunderstood parts of your first reply. – Tim Flynn Jan 01 '15 at 00:01
  • Ok, so, your answer still has the problem, except that instead of breaking from the loop early it gonna burn 100% CPU in the `wait_for` call. The reason it doesn't work is mentioned [here in parameter description](https://en.cppreference.com/w/cpp/thread/condition_variable/wait_for), it says "Note that rel_time must be small enough not to overflow when added to std::chrono::steady_clock::now()". Other than using conditionally `wait` or `wait_for` call, I don't see a way to work around that problem, because substracting from `max` changes the type to an incompatible one. – Hi-Angel Sep 18 '19 at 09:32
1

The reason for the problem is this nasty bit in the description for rel_time parameter:

Note that rel_time must be small enough not to overflow when added to std::chrono::steady_clock::now().

So when you do m_pushCondition.wait_for(lock, std::chrono::seconds::max()); the parameter overflows inside the function. In fact, if you enable undefined sanitizer, (e.g. -fsanitize=undefined option for GCC and Clang), and run the app, you may see the following runtime warning:

/usr/include/c++/9.1.0/chrono:456:34: runtime error: signed integer overflow: 473954758945968 + 9223372036854775807 cannot be represented in type 'long int'

Worth noting though that for some reason I did not have this warning for the actual app I was working on, probably a sanitizer bug. Anyway.

So what you can do. First: do not try to work around that by simply using the wait_for() overload with predicate because you gonna make yourself a bad spinlock burning your CPU core. Second: substracting max() - now() doesn't seem to work because it changes the type.

One way to work that around is using conditionally condition_variable::wait() and condition_variable::wait_for().

Another one may be to just declare declare big timespan, and use it. E.g.:

// This is a replacement to chrono::seconds::max(). The latter doesn't work with
// `wait_for` call because its `rel_time` parameter description has the following
// sentence: "Note that rel_time must be small enough not to overflow when added to
// std::chrono::steady_clock::now()".
const chrono::seconds many_hours = 99h;

// …[snip]…
    m_pushCondition.wait_for(lock, many_hours);
// …[snip]…

You probably can tolerate a "spurious" wakeup once a 99 hours :)

Hi-Angel
  • 4,933
  • 8
  • 63
  • 86