Description
This is related to a similar issue regarding wait_for
reported in #37166 (though since the behaviour of wait_for
is specified in terms of wait_until
, this is the more fundamental issue).
In condition_variable::wait_until
when abs_time
is less than or equal to Clock::now()
, lock.unlock()
is not called, and cv_status::timeout
is returned immediately. This behaviour does not conform to the standard, which says:
18 Effects:
—(18.1) Atomically calls lock.unlock() and blocks on *this.
—(18.2) When unblocked, calls lock.lock() (possibly blocking on the lock), then returns.
—(18.3) The function will unblock when signaled by a call to notify_one(), a call to notify_all(),
expiration of the absolute timeout (32.2.4) specified by abs_time, or spuriously.
—(18.4) If the function exits via an exception, lock.lock() is called prior to exiting the function.
This clearly and unambiguously states that lock.unlock()
should always be called, regardless of the value of abs_time
. Indeed, not calling lock.unlock()
introduces the possibility of livelock, where one thread is infinitely looping on wait_until
, while another thread is unable to acquire the mutex in order to change the guarded state such that the original thread exits the loop. For example:
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>
using namespace std::chrono_literals;
int main()
{
std::mutex mutex;
std::condition_variable cv;
//std::condition_variable_any cv;
bool stop = false;
std::thread t([&]() {
std::this_thread::sleep_for(2s);
{
std::unique_lock lock(mutex);
stop = true;
}
cv.notify_all();
});
const auto t0 = std::chrono::steady_clock::now();
{
std::unique_lock lock(mutex);
while (!stop) {
cv.wait_until(lock, std::chrono::steady_clock::time_point());
//cv.wait_for(lock, 0s);
if (std::chrono::steady_clock::now() - t0 > 4s) {
lock.unlock();
t.join();
return 1;
}
}
}
t.join();
}
This example is representative of a real-life case where a worker thread is processing a list of tasks scheduled for execution at specific times. In the situation where there is always a task ready for execution, the main thread will block forever waiting to acquire the mutex, as the worker thread never yields (because wait_until
never calls lock.unlock()
).
It's worth noting that condition_variable_any
does not seem to have the same problem.
Also note that libstdc++ implements the correct behaviour as per the standard.