condition_variable
timed_wait
multithreading
C++ concurrency
synchronization

Implementing condition_variable timed_wait correctly

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Timed waits on condition variables are easy to get subtly wrong because wakeups are not guarantees. A thread can wake up spuriously, another thread can consume the condition first, and relative timeouts can accidentally stretch longer than intended if you restart them in a loop. The correct pattern is to protect shared state with a mutex and wait against a predicate tied to an absolute deadline.

Always Wait for a Predicate

The condition variable itself does not store the condition. Your shared state does. The thread should therefore wait until a predicate over that state becomes true.

cpp
1#include <condition_variable>
2#include <mutex>
3
4std::mutex mtx;
5std::condition_variable cv;
6bool ready = false;
7
8void wait_until_ready() {
9    std::unique_lock<std::mutex> lock(mtx);
10    cv.wait(lock, [] { return ready; });
11}

The predicate matters because condition variables can wake up without the condition actually being satisfied.

Timed Wait with an Absolute Deadline

For timeout behavior, prefer an absolute deadline with wait_until rather than repeatedly using a fresh relative duration in a loop.

cpp
1#include <chrono>
2#include <condition_variable>
3#include <mutex>
4
5std::mutex mtx;
6std::condition_variable cv;
7bool ready = false;
8
9bool wait_for_ready(std::chrono::milliseconds timeout) {
10    auto deadline = std::chrono::steady_clock::now() + timeout;
11    std::unique_lock<std::mutex> lock(mtx);
12
13    return cv.wait_until(lock, deadline, [] { return ready; });
14}

This returns true if ready becomes true before the deadline and false if the wait times out.

Using steady_clock is important because it is monotonic and not affected by wall-clock adjustments.

Why Relative Waits Can Go Wrong

A common mistake is:

cpp
while (!ready) {
    cv.wait_for(lock, std::chrono::seconds(1));
}

This looks reasonable, but it is wrong if you mean "wait at most one second total". A spurious wakeup after 900 milliseconds restarts the full one-second timer, so the total wait can silently exceed the intended bound.

By computing the deadline once and waiting until that fixed point, you avoid timeout inflation.

Signaling Thread Pattern

The waiting side is only half of the story. The notifying side must update the shared state while holding the same mutex, then notify waiting threads.

cpp
1void mark_ready() {
2    {
3        std::lock_guard<std::mutex> lock(mtx);
4        ready = true;
5    }
6    cv.notify_all();
7}

The lock ensures the state change and the waiting predicate are synchronized correctly. The notification tells waiters to re-check the predicate.

Full Example with Shared Queue

Here is a small producer-consumer style example using a timed wait:

cpp
1#include <chrono>
2#include <condition_variable>
3#include <mutex>
4#include <optional>
5#include <queue>
6
7std::mutex queue_mutex;
8std::condition_variable queue_cv;
9std::queue<int> queue_data;
10
11std::optional<int> pop_for(std::chrono::milliseconds timeout) {
12    auto deadline = std::chrono::steady_clock::now() + timeout;
13    std::unique_lock<std::mutex> lock(queue_mutex);
14
15    if (!queue_cv.wait_until(lock, deadline, [] { return !queue_data.empty(); })) {
16        return std::nullopt;
17    }
18
19    int value = queue_data.front();
20    queue_data.pop();
21    return value;
22}

This is the usual shape of correct timed waiting in real code: lock, predicate, absolute deadline, then consume the shared state.

Common Pitfalls

The biggest mistake is treating notify_one or notify_all as proof that the condition is now true. Notifications only mean "check again". The predicate still decides whether the waiting thread should proceed.

Another issue is waiting without a predicate. That makes the code vulnerable to spurious wakeups and races where another thread changes the state between notification and reacquisition of the mutex.

Using a relative timeout repeatedly in a loop is another classic bug. It often turns a one-second intended limit into an arbitrarily long wait under repeated wakeups.

Finally, use steady_clock for deadlines. System clock jumps can make wall-clock-based timeouts behave unpredictably.

Summary

  • Condition variables wait for shared state, not for notifications by themselves.
  • Always pair a timed wait with a predicate protected by the same mutex.
  • Use wait_until with a single absolute deadline when you need a true overall timeout.
  • Handle signaling by changing the shared state first and notifying after that.
  • Prefer steady_clock so timeout behavior is stable even if the system clock changes.

Course illustration
Course illustration

All Rights Reserved.