Skip to content

condition_variable::wait_until is not standard-conforming and can cause livelocks #53928

Open
@hpesoj

Description

@hpesoj

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    libc++libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.threadingissues related to threading

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions