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

C++11——并发库介绍

说明本篇旨在介绍并发库怎么使用介绍的不会太全面。前置条件是了解 Linux/window 的相应的系统调用再看。这是文档cplusplus。1、thread库1.1 说明其实 C 的 thread 库就是对系统调用封装了一层其实不仅是它我下面介绍的所有库都是这样其实用系统调用也是可以实现的不过它支持跨平台这是一个很大的优势另外它还融合了一些 C 的特性总体来说用起来会比系统调用要舒服。。1.2 函数说明1.2.1 构造函数下面是构造一个线程对象的方式可以看到它支持创建一个空线程后续可通过移动构造为线程函数体赋值注意它不支持拷贝构造一般是第二种用的最多。下面是定义线程的一些方式#include iostream #include thread void print(int num) { std::cout std::this_thread::get_id() : num std::endl; } int main() { std::thread t1; t1 std::thread(print, 1); //这里是移动赋值 std::thread t2(std::thread(print, 2)); //这里是移动构造 std::thread t3(print, 3); //这里使用了第二种构造 t1.join(); t2.join(); t3.join(); return 0; }大家应该发现了 std::thread t3(print, 3) 它没有写显示的写类型。这是因为它是函数编译器会自动推导类型。1.2.2joinable()用于判断一个线程是否可被 join。可 join 了返回 true否则返回 false。1.2.3 get_id()它就是返回线程的 id 号。注意到返回值是 id 它其实是一个类正常来说返回一个无符号整数就行这里之所以返回类是为了在不同环境下做适配。它支持比较大小像、、、等、流插入和提取、特化了hash仿函数用于被哈希存储从而实现快速查找。它一般是线程对象用于获取线程 id 时使用对于在执行体里面获取 id 需要用到 this_tread。1.3 this_thread它是一个命名空间它的 get_id() 用于在执行体中获取线程 id 。yield() 是主动让出 CPU让其他线程先执行该线程会被放到同一优先级线程的末尾。sleep_for 和 sleep_until 都是让线程休眠。sleep_for 是休眠多久sleep_until 是休眠到什么时候。下面介绍它的时候需要用到 C 的 chrono库它是时间库它在文章末尾看完这个在继续看。void print(int num) { std::this_thread::sleep_for(std::chrono::seconds(1)); // 该线程休眠一秒 std::cout std::this_thread::get_id() : num std::endl; }void print(int num) { auto start std::chrono::steady_clock::now(); auto end start std::chrono::seconds(5); std::this_thread::sleep_until(end); std::cout std::this_thread::get_id() : num std::endl; }2、mutex库2.1 说明其内部主要封装了互斥锁机制。下面是它的常见的类2.2 mutexes最常见的就是 mutex 即互斥锁。可以看到它仅支持默认构造不支持拷贝构造。下面是他的成员函数其中 lock() 是加锁、try_lock() 是尝试去加锁如果能加锁它会加锁然后返回 true不能加锁这直接返回 false、unlock() 是解锁。下面是一个用例int cnt 0; std::mutex mtx; void print(int num) { mtx.lock(); for (int i 0; i num; i) cnt; mtx.unlock(); } int main() { std::vectorstd::thread ths(2); for (int i 0; i 2; i) ths[i] std::thread(print, 10000000); for (int i 0; i 2; i) ths[i].join(); std::cout cnt; return 0; }可以看到 cnt 和 mtx 都被定义到了全局如果想在函数内部使用会怎么样呢也就是下面的这种写法void print(int num, int cnt, std::mutex mtx) { mtx.lock(); for (int i 0; i num; i) cnt; mtx.unlock(); } int main() { int cnt 0; std::mutex mtx; std::vectorstd::thread ths(2); for (int i 0; i 2; i) ths[i] std::thread(print, 10000000, cnt, mtx); for (int i 0; i 2; i) ths[i].join(); std::cout cnt; return 0; }运行发现报错了这是因为在感官上我们认为是直接把引用传递实际并不是。因为这里用到了可变参数在底层会创建一个结构体用来存放函数指针和它的参数这就相当于把值拷贝了一份但是由于 mutex 不支持拷贝所以就会报错。所以就引入了 ref 函数。所以赋值的那行就可以这么写ths[i] std::thread(print, 10000000, std::ref(cnt), std::ref(mtx));由于 C11 引入了 lambda 表达式所以就可以这样写int main() { int cnt 0; std::mutex mtx; std::vectorstd::thread ths(2); auto print [cnt, mtx](int num) { mtx.lock(); for (int i 0; i num; i) cnt; mtx.unlock(); }; for (int i 0; i 2; i) ths[i] std::thread(print, 10000000); for (int i 0; i 2; i) ths[i].join(); std::cout cnt; return 0; }lambda 表达式本质上就是仿函数也就是类捕捉列表相当于使它的成员变量因为是引用捕捉所以它们的类型是引用也就相当于持有本身。在打包的时候只是拷贝了这个 lambda 对象本身它的成员并没有被拷贝。timed_mutex 和 mutex 没有什么大的区别他只是多加了两个函数 try_lock_for 和 try_lock_until 它们和 try_lock 类似只不过多加了个休眠。try_lock_for 是如果没有申请到锁就休眠一段时间如果在这段时间内申请到锁就返回true没锁的时候就会被挂起不会一直占据CPU有锁的时候在给它唤醒让他去申请锁时间到还没有申请到就返回 false。try_lock_until 它和 try_lock_for一样不过他是到某个时间点被唤醒。recursive_mutex 和 mutex 没有什么大的区别它是避免由于递归造成的死锁。就像一个线程申请到了锁因为该函数体是递归调用所以会再次去申请它已经申请到的锁从而造成死锁。2.3 lock_guardlock_guard 是 C11 提供的支持 RAII 方式管理互斥锁资源的类。也就是在构造的时候申请锁析构的时候释放锁这样可以有效的帮助因为异常而造成死锁问题。第二个构造可以传递 adopt_lock_t 类型的 adopt_lock 对象用来管理已经 lock 的锁。下面是它的定义可以看到它是一个空类并且定义了一个 adopt_lock 常量。int cnt 0; std::mutex mtx; // 法一 void print(int num) { { std::lock_guardstd::mutex _lock(mtx); for (int i 0; i num; i) cnt; } } // 法二 //void print(int num) //{ // mtx.lock(); // { // std::lock_guardstd::mutex _lock(mtx, std::adopt_lock); // for (int i 0; i num; i) // cnt; // } //} int main() { std::vectorstd::thread ths(2); for (int i 0; i 2; i) ths[i] std::thread(print, 10000000); for (int i 0; i 2; i) ths[i].join(); std::cout cnt; return 0; }它没有成员函数2.4 unique_lock它就是 lock_guard 的升级版可以看到在构造的时候多了很多种新增了时间它是用来管理 timed_mutex 的构造时会调用try_lock_for 或 try_lock_until还有多了两个类型try_to_lock_t 和 defer_lock_t它们和上面的adopt_lock_t 类似都只是一个空类没有任何数据只用来区分调用哪个构造函数。在它的内部有申请锁的成员函数try_to_lock 是去尝试申请锁调用 try_lock 函数去申请。但如何判断是否成功拿到锁unique_lock中重载了 bool可以用它来判断具体如下void print(int num) { std::unique_lockstd::mutex _lock(mtx, std::try_to_lock); if (_lock) std::cout 拿到锁了 std::endl; else std::cout 没拿到锁 std::endl; }defer_lock_t 是延迟申请锁std::mutex mtx; void func1() { mtx.lock(); std::unique_lockstd::mutex lock(mtx, std::adopt_lock); } void func2() { std::unique_lockstd::mutex lock; lock std::unique_lockstd::mutex(mtx, std::defer_lock); lock.lock(); } int main() { std::thread t1(func1); std::thread t2(func2); t1.join(); t2.join(); return 0; }2.5 lock() 和 try_lock()它的版本有很多像上面的 unique_lock、mutexs 中都有它的身影。不过这里我要介绍一下全局的。全局的 lock 和 try_lock 是支持申请多个锁最少也是两个。在申请多个锁的时候如果其中某一个锁申请失败会立即释放掉之前所有申请成功的锁然后再次重新申请。程序会一直卡在这里直至所有锁申请成功。它和 lock 基本一样不过它如果申请失败会返回申请失败锁的下标从0开始如果申请成功会返回 -1。2.6 call_once()该函数只会被线程执行一次。std::once_flag它是一个类保证某段代码在整个程序生命周期里只执行 1 次。std::once_flag flag; void print() { std::cout 我执行了; } void func1() { std::call_once(flag, print); } int main() { std::thread t1(func1); std::thread t2(func1); t1.join(); t2.join(); return 0; }三、atomic库3.1 说明因为锁的消耗较大所以就有了原子库用来实现无锁编程。它里面的所有成员函数都是原子操作但是并不是所有类型都可以被当作原子类型必须得符合以下判断std::is_trivially_copyableT::value std::is_copy_constructibleT::value std::is_move_constructibleT::value std::is_copy_assignableT::value std::is_move_assignableT::value std::is_sameT, typename std::remove_cvT::type::value只要有一个返回false那它就不能被认为可以当作原子类型。一般来说int、bool、指针可以当作原子类型。还有一个原因就是要支持加减操作。atomic 的原理主要是通过硬件实现的现代处理器提供了原子指令来支持原子操作。原子指令可以实现对内存读取、比较和写入操作是不可分的简称 CAS 操作。另外硬件也采用了缓存一致性因为有多个处理器时它们的缓存可能有数据一致性问题。Linux/windows 分别提供了CAS的系统调用接口在C11中对它又进行了封装// C11⽀持的CAS接⼝ template class T bool atomic_compare_exchange_weak (atomicT* obj, T* expected, T val) noexcept; template class T bool atomic_compare_exchange_strong (atomicT* obj, T* expected, T val) noexcept;obj 是原子对象expected是期望值val是要改的新值。如果原子对象的值等于期望值那么就把原子对象的值换成新值如果原子对象的值不等于期望值那就代表原子对象的值被其他线程修改了那么就把期望值改成原子对象的值然后再次进行判断。大概的样子如下std::atomicint acnt 0; void Add(std::atomicint cnt) { int old cnt.load();// 获取原子变量的值 while (std::atomic_compare_exchange_weak(cnt, old, old 1)); } void func1() { for (int i 0; i 10; i) { acnt; //可以认为是这样实现的 Add(acnt); } }下面是atomic库中的CAS接口// C11中atomic类的成员函数 bool compare_exchange_weak (T expected, T val, memory_order sync memory_order_seq_cst) noexcept; bool compare_exchange_strong (T expected, T val, memory_order sync memory_order_seq_cst) noexcept;memory_order 是内存顺序 memory_order 选项CPU和编译器会偷偷乱序执行代码在多线程下一个线程内部前面和后面代码的执行顺序被颠倒了本质是为了榨干硬件性能。由于它比较复杂感兴趣的可以自己去看看。3.2 compare_exchange_weak() 和 compare_exchange_strong() 区别compare_exchange_weak() 在某些平台下即使原子变量的值等于预期值也会返回 false 这叫伪失败。在某些架构下就可能会出现像RISCV 架构它的LL/SC 指令LL/SC 指令天生就会 “伪失败”即使值一样在读取和写入的时候被中断线程被切走等SC就会失败也就是指令失败了所以返回 falseLL和SC是一对指令在LL执行完线程可能会被切走即使这样它也是原子的因为要么后面SC更新值要么直接返回旧值所以还是原子的compare_exchange_strong() 保证只要相等就一定会返回 true因为它内部自带循环自动把指令失败重试到成功为止通常来说 compare_exchange_weak() 要比 compare_exchange_strong() 要更快。3.3 自旋锁实现下面是用 atomic 极简实现了一个自旋锁class spinlock { public: spinlock() :flag(false) {} void lock() { while (flag.exchange(true)) ; } void unlock() { flag.store(false); } private: std::atomicbool flag; };exchange是修改原子变量值然后返回它上一次值。store是修改原子变量值。这段代码就是当lock() 时将变量改为 true 然后返回 false由于返回 false 循环结束即代表加锁成功。如果变量的值本来就是 true 那么会一直循环下去。解锁就是把变量的值设置为 false 即可。其实还有 atomic_flag它就是一个标志它是专门用来实现自旋锁。test_and_set 就类似于 exchange 它是把值设为 true 然后返回 false。clear 就是把值设置为 false。class spinlock { public: void lock() { while (flag.test_and_set()) ; } void unlock() { flag.clear(); } private: std::atomic_flag flag ATOMIC_FLAG_INIT; };四、condition_variable库4.1 说明它就是在系统调用的基础上又封装了一层没什么好说的wait() 需要传入unique_lockmutex类型的互斥锁。它会阻塞线程阻塞前先把锁释放然后在休眠直至被 notify 唤醒被唤醒后会再次去申请锁然后在判断。对 wait_for()、wait_until() 它和 wait() 一样不过就只是加了个时间而已。wait_for() 在等待的期间内如果被唤醒则返回 true时间到了还没被唤醒就返回 false。wait_until() 是判断是否到某一时间点。notify_one() 是随机唤醒一个线程notify_all() 是唤醒所有线程。提一下对于想使用其他类型的锁有 condition_variable_any不过这里我就不展开说了感兴趣的可以自己去查查文档。4.2 经典面试题两个线程交替打印奇数和偶数大家可以先思考一下下面是实现代码int size 100; bool flag true; std::mutex mtx; std::condition_variable cv; //打印奇数 void odd() { int i 1; while (i size) { std::unique_lockstd::mutex lock(mtx); while (flag) cv.wait(lock); std::cout i std::endl; i 2; flag true; cv.notify_one(); } } //打印偶数 void even() { int i 0; while (i size) { std::unique_lockstd::mutex lock(mtx); while (!flag) cv.wait(lock); std::cout i std::endl; i 2; flag false; cv.notify_one(); } }上面的代码其实写的很巧妙线程的启动有三种情况1.odd 线程先启动2.even 线程先启动3.同时启动。第一种情况odd 申请到了锁even 没有申请到锁他被放到阻塞队列中。odd 继续向下执行在 while 中判断发现符合条件则休眠这时候它会释放申请的锁然后 even 被唤醒它拿到锁向下执行发现不符合 while 条件则继续向下执行之后将 flag 改为 false 然后唤醒一个线程也就是odd。这时候lock锁也会被释放因为单次循环执行完毕。然后它们接着竞争锁如果是odd申请到那就执行如果还是 even 那么会从新开始运行注意这里的 flag 已经是 false 了在 while 判断时会失败该线程进入阻塞。接着唤醒 odd 线程重复上述的步骤。对于剩下的第二种也类似。第三种细分情况就是第一种或第二种。五、future这个我之前写过这是链接C11——异步_c有没有异步操作-CSDN博客六、线程池如果线程池的实现也在这篇文章就会造成该篇文章太长了我会把它放到我的下一篇中。七、chrono库7.1 介绍对于它的使用主要就是要用到 duartion 和 time_point 这两个类。7.2 duartion它表示一段时间间隔。Req 是数值类型存储时长的计数如 int/ long long 等Period 是时间单位默认 ratio1 是1秒。为了方便使用库已经定义好了常用的时长就相当于是 typedef了一下std::chrono::seconds s(2); // 2秒7.3 time_point它表示一个具体的时间点。Clock 是时钟类型提供时间基准常用system_clock / steady_clockDuartion 是时间精度有纳秒、秒等。system_clock它是类模板表示系统实时时钟对应北京时间可转成日期。下面是它的成员函数now获取当前时间。to_time_t 把 time_point 类型 time_t 从而适配C语言。steady_clock它是类模板表示单调时钟只增不减适合计算代码耗时。下面是它的成员函数定义 time_point 对象using namespace std::chrono; time_pointsystem_clock start system_clock::now();不过这种方式挺不爽的在system_clock 和 steady_clock 中的内部有这样的代码以system_clock举例namespace std::chrono { struct system_clock { // 别名把 time_pointsystem_clock 简写为 system_clock::time_point using time_point std::chrono::time_pointstd::chrono::system_clock; }; }所以就可以这么写using namespace std::chrono; system_clock::time_point start system_clock::now();time_point 允许加减 duartion 类型它的返回值是 time_point time_point 相互减的返回值是 duartion 类型。system_clock::time_point now system_clock::now(); // 时间点 时长 新时间点 auto future now seconds(10); ------------------------------------------------------------- auto start steady_clock::now(); auto end steady_clock::now(); // 两个时间点相减 → 返回时长类型 duration steady_clock::duration diff end - start;
http://www.rkmt.cn/news/1387001.html

相关文章:

  • 别再死记硬背Floyd算法了!用动态规划思想拆解‘多源最短路径’问题(附Java/Python代码)
  • 告别Unity默认Text!手把手教你用TextMeshPro打造炫酷UI文字(附中文字体制作避坑指南)
  • 具身智能的发展面临哪些挑战?
  • 编程语言、存储技术、数据结构、数学矩阵和系统可靠性设计范畴
  • STM32CubeMX保姆级教程:从零点亮STM32F103C8T6最小系统板的LED
  • 避坑指南:ESP32-CAM RTSP视频流延迟高、卡顿?可能是这几个配置没调好
  • GPT-5.5编程助手:全栈开发的第三只手
  • 当工控系统遇上APT:用Python模拟Stuxnet对西门子S7-315 PLC的读写攻击逻辑
  • AI传动系统与燃料
  • 【物联网】使用MQTTX与OneNET云平台进行模拟MQTT协议通信
  • 告别卡顿!优化STM32+LVGUI刷新率的实战心得:从帧缓冲区、心跳时钟到DMA2D配置
  • 别再乱用USB转串口了!手把手教你搞定山特UPS(C3K/C3KS)与电脑的串口直连
  • 拆解美阔65W氮化镓充电器:看MGZ31N65这颗集成GaN芯片如何搞定1A2C
  • UE5多人联机开发:从游戏大厅到玩家生成的完整蓝图流程(含游戏实例传参)
  • 为什么92%的DeepSeek私有化部署项目在第3周崩溃?——5类典型耦合陷阱与解耦模板
  • Unity游戏性能优化第一步:用SystemInfo精准识别玩家硬件(附CPU/显卡/内存检测代码)
  • UE4新手教程:用蓝图实现按1、2键快速切换操控不同角色(附4.23.1版本节点详解)
  • OpenGL地球渲染踩坑实录:GLFW、GLUT、FreeGLUT到底怎么选?附性能对比
  • TVA 登顶工业视觉的 “iPhone 时刻”(2)
  • 无线回散射技术与电压分复用架构在物联网传感中的应用
  • Unity编辑器模拟手机大退重连工具类
  • 隧道裂缝剥落病害AI识别系统
  • Veo 2提示词效能跃迁实战(工业级Prompt链构建全图谱)
  • 2026年5月更新:昆明广告纸杯订购厂家选择指南与实力解析 - 2026年企业推荐榜
  • 3.Hermes皮肤,别只会换颜色
  • 【性能优化】如何通过调整模型上下文大小与 Prompt 缩减 Midscene 运行耗时?
  • YOLOv8结核病识别检测系统(项目源码+YOLO数据集+模型权重+UI界面+python+深度学习+环境配置)
  • Shift-JIS编码探秘:从Windows 10实战到编码原理深度解析
  • 跳过Win11微软账户登录后,别忘了关BitLocker!本地账户的数据安全避坑指南
  • 东方通TongWeb部署实战:从Xshell报错到成功启动服务的完整避坑记录