admin管理员组

文章数量:1584624

理解 C++ 中的条件变量(Condition Variable)

在多线程编程中,我们常常需要一个线程等待某个条件的变化,比如等待数据的生成或某个标志位的设置。如果没有条件变量(condition_variable),线程可能会使用忙等待(不断检查条件是否满足),这会导致 CPU 资源的浪费。条件变量提供了一种高效的等待机制,使线程在等待条件时进入休眠状态,不占用 CPU 资源,当条件满足时被唤醒继续执行。

条件变量的基本概念

条件变量允许一个或多个线程同时阻塞。一般情况下,生产者线程利用支持 std::mutexstd::lock_guardstd::unique_lock 修改共享变量后,并通知条件变量。消费者线程获取同一个 std::mutex(由 std::unique_lock 持有),并调用 std::condition_variablewaitwait_forwait_untilwait 操作会释放互斥量,同时挂起该线程。当条件变量收到通知、超时到期或发生虚假唤醒时,线程被唤醒,互斥量也会被原子地重新获取。如果是虚假唤醒,线程应该检查条件并继续等待,以保证业务的正确性。

注意事项

  1. 使用 unique_lock 而不是 lock_guard
    在等待时,管理 mutex 使用的是 unique_lock 而不是 lock_guard,因为等待时是不持有锁的。wait 函数会调用 mutexunlock 函数,之后再睡眠,直到被唤醒后才持有锁。lock_guard 没有 lock/unlock 接口,所以需要用 unique_lock

  2. 伪唤醒
    在对 wait 函数的调用中,条件变量可能会对提供的条件检查任意多次。这发生在互斥元被锁定的情况下,并且当测试条件返回 true 时就会立即返回。当等待线程重新获取互斥元并检测条件时,如果它并非直接响应另一个线程的通知,这就是所谓的伪唤醒(spurious wake)。伪唤醒的次数和频率根据定义是不确定的。

  3. 通知丢失
    如果发送方在接收方进入等待状态之前发送通知,则通知会丢失。

使用场景

  1. 生产者-消费者问题
    生产者线程生成数据并通知消费者线程处理数据。

  2. 任务队列
    多个线程从一个任务队列中获取任务并处理,当队列为空时,线程进入等待状态,直到有新的任务被添加。

  3. 事件等待
    一个或多个线程等待某个事件的发生,当事件发生时,唤醒等待的线程进行处理。

std::condition_variablestd::condition_variable_any 的区别

  1. 互斥锁类型

    • std::condition_variable 只与 std::unique_lock<std::mutex> 类型的锁配合使用。这意味着它只能与 std::mutex 类型的互斥锁一起使用。
    • std::condition_variable_any 可以与任何符合基本锁(BasicLockable)和锁互换(Lockable)概念的锁对象配合使用。它不仅可以与 std::mutex 配合,还可以与其他类型的锁(例如 std::shared_mutex、用户定义的互斥锁等)一起使用。
  2. 灵活性

    • std::condition_variable 更加专用,提供了一种高效的实现,因为它只支持 std::unique_lock<std::mutex>
    • std::condition_variable_any 更加通用,提供了更大的灵活性,可以与任何符合要求的锁一起使用,因此在某些情况下更为便利。

为什么要有 std::condition_variable_any

std::condition_variable_any 的引入是为了提供更大的灵活性,允许开发者使用不同类型的锁来实现同步需求。在某些情况下,开发者可能需要使用自定义的锁类型或者 std::shared_mutex 这样的共享锁,这时 std::condition_variable 就无法满足需求,而 std::condition_variable_any 则可以适应这些情况。

代码示例

以下是一个使用条件变量的代码示例,演示了如何在 C++ 中使用 std::condition_variable 进行线程同步:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>

std::mutex read_mutex_;
std::condition_variable condition_variable_;
bool readFlag_ = false;

void do_print_id(int id) {
    std::unique_lock<std::mutex> uniqueLock(read_mutex_);
	  /**
   * todo -
   * 只有当 __pred 条件为 false 时调用 wait() 才会阻塞当前线程,并且在收到其他线程的通知后只有当 __pred 为 true 时才会被解除阻塞
   * 1. 线程第一次执行到这里时,会执行表达式方法。判断到 ready 为false。则当前线程阻塞在此,同时解锁互斥量,不影响其他线程获取锁
   * 2. 当线程被唤醒,首先就是不断的尝试重新获取并加锁互斥量,若获取不到锁就卡在这里反复尝试加锁
   * 3. 若获取到了锁,就执行表达式方法,然后继续往下执行
   *
   * 函数原型:void condition_variable::wait(unique_lock<mutex>& __lk, _Predicate __pred)
   */
    condition_variable_.wait(uniqueLock, [&] {
        std::cout << "condition_variable wait.id: " << id << std::endl;
        return readFlag_;
    });

    std::cout << "do_print_id -> thread : " << id << std::endl;
}

class QConditionVariable {
public:
    void task1() {
        std::thread threads[10];
        for (int i = 0; i < 10; ++i) {
            threads[i] = std::thread(do_print_id, i);
        }

        for (std::thread &item : threads) {
            item.detach();
        }
        std::this_thread::sleep_for(std::chrono::seconds(2));
        notify();
        std::this_thread::sleep_for(std::chrono::seconds(5));
        std::cout << "task1--end" << std::endl;
    }

    void task2() {
        std::unique_lock<std::mutex> uniqueLock(read_mutex_);

        std::cv_status cvStatus = condition_variable_.wait_for(uniqueLock, std::chrono::seconds(5));

        if (cvStatus == std::cv_status::no_timeout) {
            //表示条件变量等待成功(条件满足或被通知)。
        } else if (cvStatus == std::cv_status::timeout) {
            //表示条件变量等待超时。
        }
    }
      void task3() {
    std::unique_lock<std::mutex> uniqueLock(read_mutex_);
    /**
     * wait_for: 等待特定的时间段,直到被通知或时间到期。
     * 执行到这里时,当前线程将进入等待状态,等待最多1秒钟:
     *  如果在这1秒钟内,条件变量 condition_variable_ 被其他线程通知(通常通过 notify_one 或 notify_all),wait_for 会立即返回 std::cv_status::no_timeout,并且 while 循环会终止。
     *  如果这1秒钟内没有收到通知,wait_for 返回 std::cv_status::timeout,循环条件为真,线程继续执行循环体内的代码(这里是空的,没有其他操作),然后再次进入等待。
     */
    while (condition_variable_.wait_for(uniqueLock, std::chrono::seconds(1)) == std::cv_status::timeout) {
    }
  }

private:
    void notify() {
        std::cout << "start----notify" << std::endl;
        std::unique_lock<std::mutex> uniqueLockNotify(read_mutex_);
        readFlag_ = true;
        condition_variable_.notify_all();
        std::cout << "end----notify" << std::endl;
    }
};

在这个示例中,我们创建了一个简单的类 QConditionVariable,包含了两个任务 task1task2task1 生成 10 个线程,每个线程都会调用 do_print_id 函数,等待条件变量 condition_variable_ 的通知。task2 则是一个等待操作示例,等待特定时间段或直到被通知。

通过条件变量,我们可以有效地管理多线程程序中的同步和通信,避免忙等待,提高程序的执行效率。

本文标签: 变量条件conditionvariable