当前位置: 首页 > news >正文

C++ 11 中 condition_variable 的探索与实践

文章目录

    • 一、条件变量的基本概念
      • 1.1 条件变量的定义
      • 1.2 条件变量与互斥锁的配合
    • 二、条件变量的基本用法
      • 2.1 常见的操作
      • 2.2 示例:生产者 - 消费者模型
      • 代码说明
    • 三、深入理解条件变量
      • 3.1 条件变量的底层实现
      • 3.2 条件变量与忙等待的对比
      • 3.3 提升性能的注意事项
        • 避免虚假唤醒
        • 最小化锁的持有时间
    • 四、条件变量的应用场景
      • 4.1 生产者 - 消费者模型
      • 4.2 读者 - 写者模型
      • 4.3 线程池
    • 五、条件变量的相关类和成员函数
      • 5.1 相关类
        • `std::condition_variable`
        • `std::condition_variable_any`
      • 5.2 相关成员函数
        • `wait()`
        • `wait_for()`
        • `wait_until()`
        • `notify_one()`
        • `notify_all()`
    • 六、条件变量的示例代码分析
      • 6.1 生产者 - 消费者模型示例
      • 代码分析
      • 6.2 线程交替打印示例
      • 代码分析
    • 七、总结
    • 八、条件变量的示意图

在现代计算机编程的广阔天地里,多核处理器的普及宛如一阵春风,吹开了多线程编程的繁花。多线程编程,这一构建高性能应用程序的利器,逐渐成为了开发者们手中的法宝。然而,在多线程程序的世界里,线程间的同步和通信就像是一座难以跨越的大山,横亘在开发者面前。C++ 标准库自 C++11 开始,正式引入了 <condition_variable>(条件变量)这一工具,如同一把利剑,为开发者们劈开了这座大山,提供了一种高效、灵活的线程同步机制。

一、条件变量的基本概念

1.1 条件变量的定义

条件变量 (Condition Variable) 是一种线程同步机制,它就像是一位公正的裁判,用于在线程之间等待某个条件的成立,并通知其他线程这一情况。它通常与互斥锁 (std::mutex) 配合使用,就像是一对默契的搭档,以保证线程间的同步和数据访问的安全性。

简单来说,条件变量可以让一个线程阻塞等待特定条件的成立,当条件满足时,其他线程可以通过通知 (notify_onenotify_all) 唤醒等待的线程。在 C++ 标准库中,条件变量通过以下两个类实现:

  • std::condition_variable:适用于普通线程同步,就像是一把精准的手术刀,专门用于特定场景。
  • std::condition_variable_any:通用版本,支持与任意的锁类型配合使用,如同一个万能的工具箱,适用于各种复杂情况。

1.2 条件变量与互斥锁的配合

条件变量和互斥锁的配合使用是线程同步的关键。互斥锁用于保护共享资源,防止多个线程同时访问导致数据不一致。而条件变量则用于在线程之间传递信号,通知线程何时可以继续执行。

当一个线程需要等待某个条件满足时,它会先获取互斥锁,然后检查条件是否满足。如果条件不满足,线程会调用条件变量的 wait 函数,释放互斥锁并进入等待状态。当其他线程修改了共享变量并满足条件时,它会调用条件变量的 notify_onenotify_all 函数,唤醒等待的线程。被唤醒的线程会重新获取互斥锁,继续执行。

这种配合方式可以避免线程的忙等待,提高程序的效率。例如,在生产者 - 消费者模型中,生产者线程会等待缓冲区中有足够空间后再生产新数据,消费者线程会等待缓冲区非空,然后从中取出数据。通过条件变量和互斥锁的配合,可以实现线程间的高效同步。

二、条件变量的基本用法

2.1 常见的操作

条件变量提供了以下几个核心操作:

  • wait:阻塞当前线程,直到条件满足或被其他线程通知。就像是一个沉睡的巨人,等待着被唤醒的那一刻。
  • notify_one:通知一个等待中的线程。如同在黑暗中点亮一盏明灯,唤醒一个沉睡的灵魂。
  • notify_all:通知所有等待中的线程。仿佛是一声嘹亮的号角,唤醒所有沉睡的勇士。

2.2 示例:生产者 - 消费者模型

条件变量的一个经典应用场景是生产者 - 消费者模型。以下是一个简单的例子:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>std::queue<int> buffer;
const unsigned int MAX_BUFFER_SIZE = 10;
std::mutex mtx;
std::condition_variable cv;void producer(int id) { for (int i = 0; i < 20; ++i) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [] { return buffer.size() < MAX_BUFFER_SIZE; }); buffer.push(i); std::cout << "Producer " << id << " produced: " << i << std::endl; lock.unlock(); cv.notify_all(); }
}void consumer(int id) { for (int i = 0; i < 20; ++i) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [] { return !buffer.empty(); }); int item = buffer.front(); buffer.pop(); std::cout << "Consumer " << id << " consumed: " << item << std::endl; lock.unlock(); cv.notify_all(); }
}int main() { std::thread prod1(producer, 1); std::thread prod2(producer, 2); std::thread cons1(consumer, 1); std::thread cons2(consumer, 2); prod1.join(); prod2.join(); cons1.join(); cons2.join(); return 0;
}

代码说明

  • 生产者线程会等待缓冲区中有足够空间后再生产新数据。当缓冲区已满时,线程会调用 cv.wait 函数,释放互斥锁并进入等待状态。当缓冲区有空闲空间时,其他线程会调用 cv.notify_all 函数,唤醒等待的生产者线程。
  • 消费者线程会等待缓冲区非空,然后从中取出数据。当缓冲区为空时,线程会调用 cv.wait 函数,释放互斥锁并进入等待状态。当缓冲区有数据时,其他线程会调用 cv.notify_all 函数,唤醒等待的消费者线程。
  • cv.wait 用于阻塞当前线程,直到条件满足为止。在调用 cv.wait 函数时,线程会自动释放互斥锁,避免死锁。
  • cv.notify_all 唤醒所有等待中的线程。当条件满足时,线程会调用 cv.notify_all 函数,通知所有等待的线程条件已经满足。

通过这个例子,我们可以看到条件变量在生产者 - 消费者模型中的重要作用。它可以实现线程间的高效同步,避免线程的忙等待,提高程序的性能。

三、深入理解条件变量

3.1 条件变量的底层实现

条件变量的底层实现通常依赖于操作系统提供的同步原语,例如 POSIX 下的 pthread_cond_t 或 Windows API 的 CONDITION_VARIABLE。C++ 标准库中的条件变量通过互斥锁 (std::mutex) 和条件变量本身相互结合,形成了一种高效的线程同步机制。

核心机制包括:

  • 等待:线程调用 wait 时,会先解锁关联的互斥锁并进入休眠状态。这样可以防止因线程阻塞导致的死锁。就像是一个聪明的旅行者,在等待的过程中不会占用太多资源。
  • 通知:调用 notify_onenotify_all 时,会唤醒一个或多个等待中的线程,并重新尝试获取互斥锁。如同一位使者,传递重要的信息,唤醒沉睡的人们。

3.2 条件变量与忙等待的对比

在没有条件变量的情况下,线程通常会采用“忙等待”的方式检查条件是否成立。这种方法会导致 CPU 资源的浪费。例如:

while (!condition) { // 忙等待,浪费 CPU 资源
}

相比之下,条件变量的优点在于:

  • 高效:线程可以在等待时进入休眠状态,而不是占用 CPU。就像是一个懂得休息的运动员,在等待的过程中保存体力。
  • 简洁:无需手动管理线程间的通信逻辑。如同一个自动化的生产线,减少了人工干预。

3.3 提升性能的注意事项

避免虚假唤醒

条件变量的 wait 函数可能会因虚假唤醒 (spurious wakeup) 而提前返回。因此,建议将 wait 的调用写成以下形式:

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

通过传入一个谓词函数,可以确保线程只有在条件成立时才会继续执行。这就像是一个严格的门卫,只有满足条件的人才能进入。

最小化锁的持有时间

条件变量的通知操作 (notify_onenotify_all) 不需要持有锁。因此,在修改共享变量并通知等待的线程时,应该尽量减少锁的持有时间,避免其他线程长时间等待。例如:

{std::lock_guard<std::mutex> lock(mtx);// 修改共享变量
}
cv.notify_one();

这样可以提高程序的并发性能,让更多的线程能够同时执行。

四、条件变量的应用场景

4.1 生产者 - 消费者模型

生产者 - 消费者模型是条件变量的经典应用场景。在这个模型中,生产者线程负责生产数据,消费者线程负责消费数据。通过条件变量和互斥锁的配合,可以实现线程间的高效同步,避免数据竞争和死锁。

4.2 读者 - 写者模型

读者 - 写者模型是另一个常见的应用场景。在这个模型中,读者线程可以同时读取共享资源,而写者线程需要独占访问共享资源。通过条件变量和互斥锁的配合,可以实现读者和写者之间的同步,避免数据不一致。

4.3 线程池

线程池是一种常见的并发编程模型,用于管理和复用线程。在线程池中,线程会等待任务的到来,当有任务时,线程会被唤醒并执行任务。通过条件变量和互斥锁的配合,可以实现线程池的高效管理,提高程序的性能。

五、条件变量的相关类和成员函数

5.1 相关类

std::condition_variable

std::condition_variable 是一个类,用于实现条件变量的基本功能。它只能与 std::unique_lock<std::mutex> 一起使用。

std::condition_variable_any

std::condition_variable_any 是一个更通用的条件变量类,它可以与任何满足可锁定 (Lockable) 要求的锁类型一起使用,而不仅仅局限于 std::unique_lock<std::mutex>

5.2 相关成员函数

wait()

使当前线程阻塞,直到收到通知或发生虚假唤醒。调用该函数时,线程会释放其所持有的锁,进入等待状态。当收到通知后,线程会重新获取锁并继续执行。

有两个重载版本如下:

  • void wait( std::unique_lock<std::mutex>& lock );:无条件等待。
  • template< class Predicate > void wait( std::unique_lock<std::mutex>& lock, Predicate pred );:等待直到 pred() 返回 true,可以避免虚假唤醒。
wait_for()

使当前线程阻塞,直到收到通知、发生虚假唤醒或达到指定的超时时间。返回值表示线程被唤醒的原因。

函数原型:template< class Rep, class Period > std::cv_status wait_for( std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep,Period>& rel_time );

wait_until()

使当前线程阻塞,直到收到通知、发生虚假唤醒或到达指定的时间点。返回值表示线程被唤醒的原因。

函数原型:template< class Clock, class Duration > std::cv_status wait_until( std::unique_lock<std::mutex>& lock, const std::chrono::time_point<Clock,Duration>& timeout_time );

notify_one()

唤醒一个等待在该条件变量上的线程。如果没有线程在等待,则该函数不做任何操作。

notify_all()

唤醒所有等待在该条件变量上的线程。

六、条件变量的示例代码分析

6.1 生产者 - 消费者模型示例

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>std::mutex mtx;
std::condition_variable cv;
std::queue<int> buffer;
const unsigned int MAX_BUFFER_SIZE = 10;void producer(int id) { int data = 0; while (true) { // 模拟生产数据的时间std::this_thread::sleep_for(std::chrono::milliseconds(100));std::unique_lock<std::mutex> lock(mtx); // 等待缓冲区未满cv.wait(lock, [] { return buffer.size() < MAX_BUFFER_SIZE; }); // 生产数据并放入缓冲区buffer.push(data);std::cout << "生产者 " << id << " 生产了数据 " << data << std::endl;data++; // 通知消费者cv.notify_all(); } 
}void consumer(int id) { while (true) {std::unique_lock<std::mutex> lock(mtx); // 等待缓冲区不为空cv.wait(lock, [] { return !buffer.empty(); }); // 从缓冲区取出数据 int data = buffer.front();buffer.pop();std::cout << "消费者 " << id << " 消费了数据 " << data << std::endl; // 通知生产者cv.notify_all(); // 模拟处理数据的时间lock.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(150)); } 
}int main() {std::thread producers[2], consumers[2]; // 启动生产者线程 for (int i = 0; i < 2; ++i) {producers[i] = std::thread(producer, i); } // 启动消费者线程 for (int i = 0; i < 2; ++i) {consumers[i] = std::thread(consumer, i); } // 等待线程完成(此示例中线程是无限循环,可根据需要修改) for (int i = 0; i < 2; ++i) {producers[i].join();consumers[i].join(); } return 0; 
}

代码分析

  • 全局变量:
    • mtx:互斥锁,用于保护共享缓冲区的访问,防止数据竞争。
    • cv:条件变量,用于线程间的等待和通知。
    • buffer:共享缓冲区,存放生产者生成的数据,供消费者消费。
    • MAX_BUFFER_SIZE:限制缓冲区的最大容量,防止过度填充。
  • 生产者函数:
    • 使用 unique_lock 获取互斥锁,确保对缓冲区的独占访问。
    • 使用 cv.wait 等待缓冲区有空间,当缓冲区已满时,线程会释放锁并进入等待状态。
    • 生产数据并放入缓冲区,然后调用 cv.notify_all 通知可能等待的消费者线程。
  • 消费者函数:
    • 使用 unique_lock 获取互斥锁,确保对缓冲区的独占访问。
    • 使用 cv.wait 等待缓冲区有数据,当缓冲区为空时,线程会释放锁并进入等待状态。
    • 从缓冲区取出数据,然后调用 cv.notify_all 通知可能等待的生产者线程。

6.2 线程交替打印示例

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>std::mutex mtx;
std::condition_variable cv;
bool oddTurn = true;void printOdd() {for (int i = 1; i <= 10; i += 2) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return oddTurn; });std::cout << i << std::endl;oddTurn = false;cv.notify_one();}
}void printEven() {for (int i = 2; i <= 10; i += 2) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return !oddTurn; });std::cout << i << std::endl;oddTurn = true;cv.notify_one();}
}int main() {std::thread t1(printOdd);std::thread t2(printEven);t1.join();t2.join();return 0;
}

代码分析

  • 全局变量:
    • mtx:互斥锁,用于保护共享变量 oddTurn 的访问。
    • cv:条件变量,用于线程间的等待和通知。
    • oddTurn:布尔变量,用于控制线程的执行顺序。
  • printOdd 函数:
    • 使用 unique_lock 获取互斥锁,确保对共享变量 oddTurn 的独占访问。
    • 使用 cv.wait 等待 oddTurntrue,当 oddTurnfalse 时,线程会释放锁并进入等待状态。
    • 打印奇数,然后将 oddTurn 设置为 false,调用 cv.notify_one 通知等待的线程。
  • printEven 函数:
    • 使用 unique_lock 获取互斥锁,确保对共享变量 oddTurn 的独占访问。
    • 使用 cv.wait 等待 oddTurnfalse,当 oddTurntrue 时,线程会释放锁并进入等待状态。
    • 打印偶数,然后将 oddTurn 设置为 true,调用 cv.notify_one 通知等待的线程。

通过这两个示例,我们可以看到条件变量在不同场景下的应用,以及如何通过条件变量和互斥锁的配合实现线程间的同步和通信。

七、总结

C++ 11 中的条件变量是一种强大的线程同步机制,它与互斥锁配合使用,可以实现高效的线程同步和通信。通过条件变量,我们可以避免线程的忙等待,提高程序的性能。

在使用条件变量时,需要注意以下几点:

  • 必须在持有互斥锁的情况下调用 wait 函数。
  • wait 函数可能会发生虚假唤醒,因此建议使用带谓词的版本。
  • 通知操作 (notify_onenotify_all) 不需要持有锁,但在修改共享变量时应该尽量减少锁的持有时间。

通过合理使用条件变量,可以解决多线程编程中的许多同步问题,如生产者 - 消费者模型、读者 - 写者模型等。希望本文能够帮助你更好地理解和使用 C++ 11 中的条件变量。

八、条件变量的示意图

条件变量的示意图

http://www.dtcms.com/a/263734.html

相关文章:

  • 解锁阿里云日志服务SLS:云时代的日志管理利器
  • 【AI 时代的网络爬虫新形态与防护思路研究】
  • iOS 越狱插件 主动调用C函数和OC函数
  • DBA 命令全面指南:核心操作、语法与最佳实践
  • 【仿muduo库实现并发服务器】Channel模块
  • 大规模分布式数据库读写分离架构:一致性、可用性与性能的权衡实践
  • opencv使用 GStreamer 硬解码和 CUDA 加速的方案
  • Java ArrayList 扩容机制
  • 【MobaXterm、Vim】使用合集1
  • 结构体实战:用Rust编写矩形面积计算器
  • Electron 沙箱模式深度解析:构建更安全的桌面应用
  • Let‘s Encrypt 免费证书使用
  • 2022/7 N2 jlpt词汇
  • STM32作为主机识别鼠标键盘
  • Vue-16-前端框架Vue之应用基础集中式状态管理pinia(一)
  • SeaTunnel 社区月报(5-6 月):全新功能上线、Bug 大扫除、Merge 之星是谁?
  • 从零到一搭建远程图像生成系统:Stable Diffusion 3.5+内网穿透技术深度实战
  • 密码学(斯坦福)
  • 数字图像处理学习笔记
  • 电机控制的一些笔记
  • CentOS Stream 下 Nginx 403 权限问题解决
  • jQuery UI 安装使用教程
  • 使用Spring Boot 原始的文件下载功能,告别下载风险!
  • Python实例题:基于 Flask 的任务管理系统
  • 数据结构:递归:组合数(Combination formula)
  • vue3中实现高德地图POI搜索(附源码)
  • 主流零信任安全产品深度介绍
  • 网络的相关概念
  • 港美股证券交易系统综合解决方案:技术架构革新与跨境服务升级
  • docker windows 安装mysql:8.0.23