Skip to content

Wiki

condition_variable

About 20 min read

std::condition_variable Used to solve this type of problem:

A certain condition is not yet satisfied. I do not want to keep occupying the CPU to check, but instead want to sleep first and wake up when the condition may have changed.

Typical scenarios include:

队列里暂时没有任务
→ worker 先睡眠

初始化还没完成
→ 其他线程先等待

消费者暂时没有数据
→ 等生产者放入数据

多个线程等待某个状态变为 ready
→ 状态变化后再继续

The most important thing about condition variables is not to memorize:

wait()
notify_one()
notify_all()

Instead, understand that it must be used together with the following set of things:

共享状态 + mutex + condition_variable + predicate

Their division of labor is:

共享状态
→ 记录“事情到底有没有发生”

mutex
→ 保护共享状态的读写

condition_variable
→ 条件不满足时让线程睡眠
→ 条件可能变化时把等待线程叫醒

predicate
→ 被唤醒以后重新判断:
  “现在到底能不能继续执行?”

Therefore:

condition_variableIt does not itself save business state; it is merely a "wait and wake-up mechanism".


1.Condition Variable Basics

1.1.Why can't it keep looping the check?

The most direct waiting method may be:

while (!ready)
{
}

This code has two problems.

First, if:

bool ready = false;

If it is an ordinary variable and multiple threads read and write it simultaneously, the following may occur:

data race

Even if changed to:

std::atomic<bool> ready{false};

Then:

while (!ready.load())
{
}

Although it is thread-safe, the thread will keep executing:

load
load
load
load
load
...

This is called:

Busy waiting

That is, although the thread is not doing anything useful, it keeps occupying the CPU to check the condition.

If it might take milliseconds, seconds, or even longer, this is usually wasteful.

The idea of a condition variable is:

条件不满足
↓
线程睡眠,不占着 CPU 空转

其他线程修改状态
↓
发出通知

等待线程醒来
↓
重新检查条件

So what the condition variable solves is not:

怎么让 bool 线程安全

Instead:

条件不满足时怎么高效等待

条件可能满足时怎么把线程叫醒

1.2.The three most basic objects of a condition variable

Usually, you will see:

std::mutex mutex;
std::condition_variable cv;
bool ready = false;

Here:

ready

is true shared state, meaning:

“现在能不能继续执行?”

mutex:

std::mutex mutex;

Responsible for protection:

ready

read and write.

cv:

std::condition_variable cv;

Responsible for:

让线程睡眠
+
把线程唤醒

The most important point:

condition_variable is not itself a state.

That is to say, the following idea is wrong:

“我收到 notify 了,所以条件肯定成立了。”

What truly determines whether the thread can continue is:

ready

Or:

!queue.empty()

or other business statuses.


1.3.Simplest wait / notify example

#include <condition_variable>
#include <mutex>
#include <print>
#include <thread>

std::mutex mutex;
std::condition_variable cv;

bool ready = false;

void worker()
{
    std::unique_lock lock(mutex);

    cv.wait(lock, [] {
        return ready;
    });

    std::println("worker start");
}

int main()
{
    std::thread thread(worker);

    {
        std::lock_guard lock(mutex);
        ready = true;
    }

    cv.notify_one();

    thread.join();
}

Output result:

worker start

The entire process can be understood as:

worker 获得 mutex
        ↓
检查 ready
        ↓
发现 ready == false
        ↓
wait():
释放 mutex
并让 worker 睡眠
        ↓
main 获得 mutex
        ↓
ready = true
        ↓
main 释放 mutex
        ↓
notify_one()
        ↓
worker 被唤醒
        ↓
worker 重新获得 mutex
        ↓
重新检查 ready
        ↓
ready == true
        ↓
wait() 返回
        ↓
打印 worker start

The most important step here is:

wait() 会在睡眠期间释放 mutex

Otherwise, the program will deadlock.


1.4.Why must the mutex be released when wait() sleeps?

Suppose the worker does this:

拿到 mutex
↓
发现 ready == false
↓
拿着 mutex 睡觉

Then main wants to execute:

ready = true;

Previously, it is also necessary to first:

lock(mutex);

But the mutex is being held by the worker the whole time.

Therefore:

worker:
等 ready 变成 true
但一直拿着 mutex

main:
想把 ready 改成 true
但拿不到 mutex

The result is:

谁也进行不下去

So the condition variable's wait() must do something very special:

当前已经持有 mutex
        ↓
原子地释放 mutex + 进入等待
        ↓
收到唤醒
        ↓
重新获得 mutex
        ↓
再返回

1.5.Why must wait() be used with std::unique_lock

Common notation:

std::unique_lock lock(mutex);

cv.wait(lock, predicate);

rather than:

std::lock_guard lock(mutex);

The reason is:

wait() Required temporarily:

unlock()

Then later:

lock()

and std::unique_lock supports this:

临时释放锁
↓
以后重新获得锁

ownership management.

std::lock_guard Manual unlocking and relocking are not supported, so it cannot satisfy the requirements of ordinary condition_variable::wait().

You can:

cv.wait(lock, predicate);

Roughly understood as:

while (!predicate())
{
    cv.wait(lock);
}

And the internal:

cv.wait(lock);

Can do:

释放 mutex
+
线程睡眠
+
被唤醒
+
重新获得 mutex

1.6.Why must “release mutex + sleep” be atomic?

There is another very important issue here.

Assume that the internal logic of wait is not atomic, but rather:

1. unlock mutex

2. 过一会儿才真正开始睡眠

Then it may happen:

worker:
unlock mutex
↓
还没真正睡下去

main:
修改 ready = true
↓
notify_one()

worker:
此时才进入睡眠

Like so:

notify 已经发生了

worker 却刚刚开始睡

the worker may keep sleeping indefinitely.

Therefore, condition variables must guarantee the following:

No gap is left between "releasing the mutex" and "entering the wait" in which a notification could be lost.

This is also why you should not use it yourself:

unlock();
sleep();
lock();

Go simulate condition_variable::wait().


2.wait and predicate

2.1.wait(lock) and wait(lock, predicate)

There are two common waiting modes for condition variables.

2.1.1.without predicate

cv.wait(lock);

It means:

先睡眠
↓
被唤醒后重新拿锁
↓
返回

The question is:

Being awakened does not mean that your business conditions are necessarily met.

So if this form is used, generally one must write the loop oneself:

while (!ready)
{
    cv.wait(lock);
}

2.1.2.with predicates

Recommended:

cv.wait(lock, [] {
    return ready;
});

It can be understood as the standard library having written it for you:

while (!ready)
{
    cv.wait(lock);
}

Therefore:

cv.wait(lock, predicate);

What is truly expressed is:

As long as the predicate does not hold yet, I will continue to wait.

For example:

cv.wait(lock, [] {
    return !queue.empty();
});

Indicates:

As long as the queue remains empty, I will continue waiting.


2.2.What is a predicate

predicate is:

The condition for determining whether the process can continue now.

For example:

[] {
    return ready;
}

Or:

[] {
    return !queue.empty();
}

Or:

[] {
    return !queue.empty() || finished;
}

Therefore:

condition_variable
→ 负责把你叫醒

predicate
→ 决定你醒来以后到底能不能继续

It can be remembered as:

notify only says "get up and take a look"; predicate says "whether it can proceed now".


2.3.Spurious wakeup

Condition variables allow a situation:

没有真正的业务事件发生
↓
wait() 却醒了

This is called:

Spurious wakeup

So this way of writing is unsafe:

cv.wait(lock);

use_shared_data();

Because:

wait() 返回

It does not necessarily mean:

共享数据已经满足条件

The correct way to write it is:

cv.wait(lock, [] {
    return ready;
});

Or equivalently:

while (!ready)
{
    cv.wait(lock);
}

In other words:

Every time it wakes up, it must re-examine the true shared state.


3.notify and status

3.1.notify is not state

There is another aspect of condition variables that is very easily misunderstood:

cv.notify_one();

Not in:

condition_variable 里面存了一条消息

It is more like:

“Those who are currently waiting for someone can stand up and re-examine the conditions.”

If notification occurs:

根本没有线程在等待

That concludes this notification.

Threads that wait later:

不会补收到这次旧通知

For example:

线程 A:
notify_one()
↓
当时没人等待
↓
通知结束

过了一会儿

线程 B:
开始 wait()

Thread B will not receive the previous notification from thread A.

This type of phenomenon is usually called:

Lost wakeup


3.2.Then why does it not easily go wrong because "the notification occurs first"

Because correct code does not rely on:

“通知有没有发生过”

Instead, it relies on:

“共享状态现在是什么”

For example:

{
    std::lock_guard lock(mutex);
    ready = true;
}

cv.notify_one();

Even if:

ready = true
notify_one()

They all occur before the worker starts to wait, so there is no problem either.

Because the worker later executes:

cv.wait(lock, [] {
    return ready;
});

It will first check:

ready

Found:

true

Therefore:

根本不会睡

Therefore:

共享状态
→ 保存事实

condition_variable
→ 只是优化等待方式

This is one of the most core understandings of condition variables.


3.3.notify_one()

cv.notify_one();

Indicates:

Wake up a thread that is currently waiting for this condition_variable.

For example, there are:

10 个 worker 都在等任务

But currently only the following have been placed:

1 个任务

Usually:

notify_one();

That is enough.

Because ultimately only one worker can consume this task.


3.4.notify_all()

cv.notify_all();

Indicates:

Wake up all threads that are currently waiting for this condition_variable.

Suitable for:

程序准备退出

全局状态发生变化

所有 worker 都应该重新检查退出条件

For example:

finished = true;
cv.notify_all();

All threads will be woken up.

However:

notify_all()

Does not represent:

所有线程同时进入临界区

After they wake up, they still need to:

竞争同一把 mutex
↓
一个一个拿锁
↓
检查 predicate

Therefore:

notify_all()
→ 所有人都醒来看看

不是
→ 所有人同时进入临界区

3.5.After modifying the state, when should notify be called?

A very common way to write this is:

{
    std::lock_guard lock(mutex);
    ready = true;
}

cv.notify_one();

The sequence is:

持锁修改共享状态
↓
释放 mutex
↓
notify

In this way, after the waiting thread is awakened:

更有机会直接拿到 mutex

rather than:

刚醒
↓
发现通知线程还拿着 mutex
↓
又继续阻塞

However, note:

notify()It does not prescribe that "it must always be placed outside the lock".

For example:

{
    std::lock_guard lock(mutex);

    ready = true;
    cv.notify_one();
}

It is also completely correct in many scenarios.

The core that truly determines correctness is:

共享谓词状态必须正确同步

wait 方必须在锁保护下检查 predicate

Putting notify outside the lock is more of a common performance and scheduling optimization habit.


4.Producer-Consumer

4.1.Producer-consumer model

This is the most classic application of condition variables.

Assumption:

producer
→ 不断往 queue 塞数据

consumer
→ 没数据时睡眠
→ 有数据时取出来处理

At the same time, it is also necessary to consider:

producer 最终会结束

Therefore, the condition that consumers are truly waiting for cannot be merely:

!queue.empty()

Also consider:

finished

Complete example:

#include <condition_variable>
#include <mutex>
#include <print>
#include <queue>
#include <thread>

std::queue<int> queue;

std::mutex mutex;
std::condition_variable cv;

bool finished = false;

void producer()
{
    for (int value = 1; value <= 5; ++value)
    {
        {
            std::lock_guard lock(mutex);

            queue.push(value);
        }

        cv.notify_one();
    }

    {
        std::lock_guard lock(mutex);

        finished = true;
    }

    cv.notify_all();
}

void consumer()
{
    while (true)
    {
        int value;

        {
            std::unique_lock lock(mutex);

            cv.wait(lock, [] {
                return !queue.empty() || finished;
            });

            if (queue.empty() && finished)
            {
                break;
            }

            value = queue.front();
            queue.pop();
        }

        std::println("consume {}", value);
    }
}

int main()
{
    std::thread producer_thread(producer);
    std::thread consumer_thread(consumer);

    producer_thread.join();
    consumer_thread.join();
}

Output:

consume 1
consume 2
consume 3
consume 4
consume 5

4.2.Why is the predicate:

!queue.empty() || finished

rather than:

!queue.empty()

Assume the producer has completely finished:

finished == true

At the same time, the queue is also empty:

queue.empty() == true

If the consumer only waits:

!queue.empty()

Then:

以后永远不会再有数据
↓
consumer 却继续等待“队列非空”
↓
没人会再生产任务
↓
consumer 永远睡下去

Therefore, the correct waiting condition should express:

有任务了

或者

生产已经结束了

That is:

!queue.empty() || finished

4.3.Why does the consumer still need to check after waking up?

After the wait is over:

cv.wait(lock, [] {
    return !queue.empty() || finished;
});

Here, only the following is explained:

队列非空
或者
生产结束

At least one is true.

So the next step is to distinguish:

4.3.1.Situation: the queue has data

queue 非空
↓
取出一个任务

4.3.2.Condition: the queue is empty, and finished == true

queue.empty()
&&
finished

Description:

现在没任务
而且以后也不会再有任务

Only then can the consumer exit safely.


4.4.Why should the actual processing of the task be placed outside the lock?

Inside the consumer:

{
    std::unique_lock lock(mutex);

    // 检查 queue
    // pop 一个任务
}

process_task();

General ratio:

std::unique_lock lock(mutex);

queue.pop();

process_task(); // 耗时 2 秒

Better.

The reason is:

If you hold the mutex while doing time-consuming work:

consumer 拿着 queue mutex 处理 2 秒

So within these 2 seconds:

producer 不能 push

其他 consumer 不能 pop

All threads are blocked by this one lock.

Therefore, within the lock, try to only do:

检查共享状态

读取 / 修改共享状态

取出任务

The truly time-consuming business logic:

放到锁外

This is a very important habit in multithreaded programs.


5.Timeouts and Advanced Usage

5.1.wait_for()

Can make the thread:

Wait at most for a period of time.

For example:

bool ok = cv.wait_for(
    lock,
    std::chrono::seconds(1),
    [] {
        return ready;
    });

Return:

true
→ 在等待结束前 predicate 成立

false
→ 超时以后 predicate 仍然不成立

For example:

if (!cv.wait_for(
        lock,
        std::chrono::seconds(1),
        [] {
            return ready;
        }))
{
    std::println("timeout");
}

Note:

timeout

It does not mean the program will necessarily fail.

It only indicates:

这段时间里条件没有成立

As for after the timeout:

重试

退出

打印日志

走备用逻辑

Determined by the business.


5.2.wait_until()

wait_until() Used to wait until a certain:

绝对时间点

For example:

auto deadline =
    std::chrono::steady_clock::now()
    + std::chrono::seconds(2);

cv.wait_until(
    lock,
    deadline,
    [] {
        return ready;
    });

It can be understood as:

一直等
直到:

ready == true

或者

到达 deadline

If it were merely:

等 2 秒

Usually:

wait_for(...)

More intuitive.

If you already have:

明确截止时间

Then:

wait_until(...)

is more appropriate.


5.3.Why is it usually recommended that the timeout steady_clock

When it involves "how long it lasts":

std::chrono::steady_clock

Generally, it is more appropriate.

Because it is a monotonic clock, it will not be affected by:

用户手动改系统时间

NTP 校时

系统时间向前 / 向后跳

but changes suddenly.

So this kind of code:

auto deadline =
    std::chrono::steady_clock::now()
    + 2s;

It is usually more appropriate than wall-clock time-based:

超时
计时
等待

5.4.Can a condition_variable wait on multiple conditions?

Of course.

What really determines the wait condition is:

predicate

For example:

cv.wait(lock, [] {
    return !queue.empty()
        || stopped
        || error;
});

Indicates:

只要下面任何一个发生:

队列有数据
或
程序停止
或
发生错误

就结束等待并继续处理

Then after waking up, make a judgment:

if (error)
{
    // 处理错误
}
else if (stopped && queue.empty())
{
    // 退出
}
else
{
    // 处理队列任务
}

Therefore:

condition_variable

It does not care how many conditions you are actually waiting for.

It is only responsible for:

睡
+
醒

Complex conditions all consist of:

predicate

Decision.


5.5.However, don't write the predicate too messily.

If you start writing:

cv.wait(lock, [] {
    return
        a && b
        || c && !d
        || error_code != 0
        || queue.size() > 3
        || ...
});

This indicates that the shared state design may already be starting to become complex.

At this point, consider:

能不能定义更清楚的状态对象

能不能使用 enum 表示状态

能不能拆分不同等待条件

能不能简化线程之间的协议

instead of piling all business logic into one giant predicate.


5.6.condition_variable_any

The standard library also includes:

std::condition_variable_any

Normal:

std::condition_variable

Main sum:

std::unique_lock<std::mutex>

Coordination.

And:

std::condition_variable_any

Can work with a broader range of lock types.

Therefore it:

更灵活

But usually:

也可能有更多开销

If it were merely:

std::mutex

Ordinary scenarios take priority:

std::condition_variable

That is enough.


5.7.condition_variable_any and stop_token

C++20 Medium:

std::condition_variable_any

There is also a very practical point:

can cooperate with std::stop_token waiting.

For this:

std::jthread

Very convenient.

For example, the worker is:

等待队列任务

At the same time, the program also hopes that:

request_stop()

Later, it can be woken up from waiting and made to exit.

At this point:

condition_variable_any
+
stop_token

we can then:

业务条件成立

或者

收到停止请求

Combine into the same waiting process.


5.8.condition_variable How to choose between atomic::wait and

After C++20:

std::atomic

Also supports:

wait()
notify_one()
notify_all()

If it were merely:

等待一个 atomic 值变化

For example:

std::atomic<bool> ready{false};

ready.wait(false);

This scenario:

atomic::wait

It will be very convenient.


If the wait condition is:

queue 非空

或者 finished == true

或者 error != 0

This category involves:

多个共享变量

容器

复杂业务状态

Usually:

mutex
+
condition_variable
+
predicate

More natural.

It can be simply remembered as:

只围绕一个 atomic 值等待
→ atomic::wait()

围绕一组共享业务状态等待
→ condition_variable

6.Common Errors

6.1.Error: treating notify as a state

Incorrect idea:

“notify 已经调用了,
所以以后 wait 一定知道。”

Incorrect.

cv.notify_one();

A message will not be permanently stored.

The real state must be placed in:

ready

Or:

queue

Or:

finished

in.


6.2.Error: Do not use predicate to handle spurious wakeups

Not recommended:

cv.wait(lock);

use_data();

Should:

cv.wait(lock, [] {
    return ready;
});

6.3.Error: The waiter locks, but the modifier performs a bare write to the shared variable

For example:

bool ready;

worker:

std::unique_lock lock(mutex);

cv.wait(lock, [] {
    return ready;
});

main but:

ready = true; // 没有锁

This may still result in a data race.

For this kind of ordinary shared state:

读和写

should all follow the same set of mutex synchronization rules.


6.4.Error: holding lock while doing time-consuming work

Error:

std::unique_lock lock(mutex);

auto task = queue.front();
queue.pop();

do_heavy_work(task);

Better:

Task task;

{
    std::unique_lock lock(mutex);

    task = queue.front();
    queue.pop();
}

do_heavy_work(task);

Release the lock as soon as possible.


6.5.Error: when stopping, only finished was modified, but notify was forgotten

For example:

{
    std::lock_guard lock(mutex);
    finished = true;
}

If the worker is:

cv.wait(...)

It may still be asleep.

Usually, the following is also required:

cv.notify_all();

Wake up the waiting threads and let them recheck:

finished

6.6.Error: Only considered "no task now", but did not consider "there will be no task in the future either"

The producer-consumer model must be designed with:

什么时候表示生产彻底结束

Otherwise, the consumer will easily:

队列空
↓
继续 wait
↓
但 producer 已经退出
↓
以后永远没人再 notify

So usually, it is necessary to:

bool finished;

or other explicit termination states.


7.Final summary

std::condition_variableThe most essential thing is not:

notify_one()

but rather this entire set of relationships:

共享状态
+
mutex
+
condition_variable
+
predicate

You can remember it like this:

共享状态
→ 记录事实

mutex
→ 保护事实

condition_variable
→ 负责睡眠和唤醒

predicate
→ 决定醒来后到底能不能继续

wait() Core Process:

拿着 mutex
↓
predicate 不成立
↓
释放 mutex + 睡眠
↓
收到 notify
↓
重新获得 mutex
↓
重新检查 predicate
↓
条件成立才真正返回

A few other key points:

notify_one()
→ 唤醒一个等待者

notify_all()
→ 唤醒所有等待者

wait_for()
→ 最多等一段时间

wait_until()
→ 最多等到某个时间点

Finally, always remember:

condition_variable There is no "memory"; what actually preserves state is a shared variable.

And:

Being woken up by notify does not mean the condition is satisfied; the predicate must always ultimately be rechecked.