C++ std::condition_variable 条件变量用法

2023-10-12 16:16:29 浏览数 (1)

1.简介

condition_variable(条件变量)是 C 11 中提供的一种多线程同步机制,它允许一个或多个线程等待另一个线程发出通知,以便能够有效地进行线程同步。

condition_variable 需要与 mutex(互斥锁)一起使用。当线程需要等待某个条件变成真时,它会获取一个互斥锁,然后在条件变量上等待,等待期间会自动释放互斥锁。另一个线程在满足条件后会获取相同的互斥锁,并调用条件变量的 notify_one() 或 notify_all() 函数来唤醒等待的线程。

条件变量是实现复杂线程同步和通信的重要工具,用于避免线程的忙等待和提高性能。

2.等待函数

condition_variable 有三个等待函数:wait()、wait_for() 和 wait_util()。

这三个函数需要与互斥锁一起使用,以互斥的方式访问共享资源,并阻塞线程,等待通知。

wait()

代码语言:javascript复制
void wait(std::unique_lock<std::mutex>& lock);

template <class Predicate>  void wait (unique_lock<mutex>& lck, Predicate pred);

wait() 函数用于阻塞线程并等待唤醒。

在调用 wait() 之前,必须获取一个独占锁(std::unique_lock)并将它传递给 wait() 函数。

如果条件变量当前不满足,线程将被阻塞,同时释放锁,使得其他线程可以继续执行。

当另一个线程调用 notify_one() 或 notify_all() 来通知条件变量时,被阻塞的线程将被唤醒,并再次尝试获取锁。

wait() 函数返回时,锁会再次被持有。

wait() 函数有一个带谓词的版本,可以简化对条件的判断。仅仅有当 pred 条件为 false 时调用 wait() 才会阻塞当前线程,解决了的唤醒丢失问题。而且在收到其它线程的通知后仅仅有当 pred 为 true 时才会被解除堵塞,解决了虚假唤醒的问题。

用法如下:

代码语言:javascript复制
std::condition_variable cv;
std::mutex mtx;
bool condition = false;

void worker() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return condition; }); // 等待条件满足
    // ...
}

wait_for()

代码语言:javascript复制
template <class Rep, class Period>
cv_status wait_for(std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep, Period>& rel_time);

template <class Rep, class Period, class Predicate>
bool wait_for (unique_lock<mutex>& lck, const chrono::duration<Rep,Period>& rel_time, Predicate pred);

wait_for() 函数用于阻塞线程并等待唤醒,但与 wait() 不同,它可以设置一个超时时间。

在调用 wait_for() 之前,必须获取一个独占锁(std::unique_lock)并将它传递给 wait_for() 函数。

如果条件变量在指定的超时时间内变为满足,线程将被唤醒,并且 wait_for() 返回 cv_status::no_timeout。

如果超时时间到期且仍未收到唤醒通知,wait_for() 返回 cv_status::timeout,线程继续执行。

wait_for() 函数同样有一个谓词版本,用法同 wait() 函数。

wait_until()

代码语言:javascript复制
template <class Clock, class Duration>
cv_status wait_until(std::unique_lock<std::mutex>& lock, const std::chrono::time_point<Clock, Duration>& abs_time);

template <class Clock, class Duration, class Predicate>
bool wait_until(unique_lock<mutex>& lck, const chrono::time_point<Clock,Duration>& abs_time, Predicate pred);

wait_until 接受一个绝对时间点作为参数。

线程将等待直到指定的绝对时间点,如果在该时间点之前条件变量满足,它将返回并继续执行。

如果到达指定时间点仍未收到唤醒通知,wait_until 返回 cv_status::timeout,线程继续执行。

wait_until() 函数同样有一个谓词版本,用法同 wait() 函数。

3.通知函数

通知函数有 notify_one() 和 notify_all()。

这两个函数都用于唤醒等待线程,以便它们可以继续执行。notify_all() 用于广播通知,以确保所有等待线程都有机会检查条件是否满足,而 notify_one() 用于选择性通知一个等待线程。

notify_one()

代码语言:javascript复制
void notify_one() noexcept;

notify_one() 用于唤醒等待在条件变量上的单个线程。

如果有多个线程在条件变量上等待,只有其中一个线程会被唤醒,具体是哪个线程 C 标准并未明确,所以是不确定的。

被唤醒的线程将尝试获取与条件变量关联的互斥锁,一旦成功获取锁,它可以继续执行。

notify_all()

代码语言:javascript复制
void notify_all() noexcept;

notify_all() 用于唤醒等待在条件变量上的所有线程。

如果有多个线程在条件变量上等待,所有这些线程都会被唤醒。

唤醒的线程将竞争获取与条件变量关联的互斥锁,然后可以继续执行。

4.注意事项

在使用 condition_variable 时需要注意以下几点:

  1. 需要与互斥量一起使用,等待前要锁定互斥量

std::condition_variable 必须与 std::unique_lock 一起使用,需要在持有 mutex 的情况下调用 wait() 函数,以确保在线程等待条件时互斥访问共享资源,从而避免竞态条件(Race Condition)。共享资源包括等待的条件,以及线程等待队列。

  1. 注意虚假唤醒和唤醒丢失

虚假唤醒(spurious wakeup)指一个或多个线程被唤醒,但没有实际的条件变化或通知发生。这些线程被认为是"虚假唤醒"。

虚假唤醒通常由操作系统或 C 标准库的实现引发,这是多线程环境中的一种正常行为。虽然它可能看起来不合理,但是在某些情况下,它是必要的,因为操作系统或标准库可能需要在内部执行一些资源管理或线程调度操作,这可能导致线程被唤醒。

唤醒丢失(wakeup loss)指发送方在接收方进入等待状态之前发送通知,结果就是导致通知消失。

为了解决虚假唤醒和唤醒丢失的问题,需要使用一个变量(通常是 bool 类型的变量)来表示等待的条件,线程在等待前和等待后检查该条件是否满足。

  1. 不要忽略 wait_for 和 wait_until 函数返回值

wait_for 和 wait_until 函数的返回值应该被检查,以判断是因为超时还是因为被通知而返回。

  1. 不要在锁内部执行耗时操作

尽量避免在锁内部执行可能会阻塞或耗时较长的操作,因为这会导致其他线程在等待条件时被阻塞。

  1. 避免死锁

确保你的线程同步逻辑不会导致死锁,例如,不要在持有互斥锁的情况下调用可能再次尝试获取同一个锁的函数。

  1. 小心使用 std::condition_variable_any

std::condition_variable_any 是通用的条件变量,可以与不同类型的互斥量一起使用。但要小心,因为它的性能可能不如与 std::mutex 直接关联的 std::condition_variable。

总之,在多线程编程中使用 std::condition_variable 时,要谨慎考虑同步逻辑,确保线程安全性,防止死锁,以及正确处理条件等待和通知。多线程编程通常很复杂,需要仔细思考和测试。

5.使用示例

代码语言:javascript复制
#include <iostream>   
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx; 			// 全局相互排斥锁。
std::condition_variable cv; // 全局条件变量。
bool ready = false;			// 全局标志位。

void print_id(int id) {
    std::unique_lock <std::mutex> lck(mtx);
    // 假设标志位不为 true, 则等待...
    while (!ready){
        // 当前线程被堵塞,等待被唤醒。
        cv.wait(lck);
    }
    // 线程被唤醒, 继续往下运行打印线程编号id。
    std::cout << "thread " << id << std::endl;
}

void go() {
    std::unique_lock<std::mutex> lck(mtx);
    ready = true;       // 设置全局标志位为 true.
    cv.notify_all();    // 唤醒全部线程.
}

int main() {
    std::thread threads[10];
    for (int i = 0; i < 10;   i) {
        threads[i] = std::thread(print_id, i);
    }

    std::cout << "10 threads ready to race..." << std::endl;
    go();

    // 等待所有线程执行完成。
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}

编译运行输出:

代码语言:javascript复制
10 threads ready to race...
thread 0
thread 1
thread 2
thread 9
thread 4
thread 6
thread 5
thread 7
thread 8
thread 3

多次运行结果是不定的,因为线程调度的顺序是不确定的。


参考文献

std::condition_variable - cplusplus.com notify_one() choose which thread to unblock? c - Why ‘wait with predicate’ solves the ‘lost wakeup’ - stackoverflow

0 人点赞