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

从一道面试题开始学习C++标准库提供的并发编程工具

一个空列表,用两个函数(只可调用一次)轮流写入值(一个写奇数,一个写偶数), 最终实现列表的值为1-100,有序排列。

简单分析:假设这两个函数分别为A和B,A函数往列表中写奇数,B函数往列表中写偶数。因为要求交替写,若A先写,则在B写一个偶数之前需要等待A先把上一个奇数写完,B写完一个偶数之后需要通知A,A写完一个奇数之后要通知B,这就存在同步关系了,自然就想到了使用条件变量。而两个函数只可调用一次,那自然想到了使用线程,让两个函数独立运行,并使用条件变量来同步写操作。

来看看使用标准库提供的并发API如何实现上述功能,代码示例如下:

#include <thread>
#include <mutex>
#include <vector>
#include <condition_variable>
#include <algorithm>
#include <iostream>std::mutex mtx;
std::condition_variable cv;
const int NUM = 100;
int current_tid = 0;    // 通过id来控制线程之间的同步顺序std::vector<int> nums(NUM);// 通过参数 tid 来标识线程
void work_odd(int tid) {for (int i = 1; i <= NUM; i++) {std::unique_lock<std::mutex> locker(mtx);cv.wait(locker, [=](){ return current_tid == tid; });if (i % 2 == 1) {nums[i - 1] = i;}current_tid = (current_tid + 1) % 2;cv.notify_one();      // 唤醒阻塞在条件变量上的一个线程}
}void work_even(int tid) {for (int i = 1; i <= NUM; i++) {std::unique_lock<std::mutex> locker(mtx);cv.wait(locker, [=](){ return current_tid == tid; });if (i % 2 == 0) {nums[i - 1] = i;}current_tid = (current_tid + 1) % 2;cv.notify_one();}
}int main()
{std::thread t1(work_odd, 0);std::thread t2(work_even, 1);t1.join();t2.join();std::for_each(nums.begin(), nums.end(), [](auto e){ std::cout << e << ' '; });std::cout << std::endl;
}

以上面的代码为例,先来快速上手一下,在标准库中,如何使用 thread 开启一个新的线程,如何使用互斥量 mutex 来互斥的访问临界区,以及如何使用条件变量 condition_variable 来实现线程之间的同步。

std::thread

thread 的声明如下所示,第一个参数为一个可调用对象,第二参数表示一个可变参数。

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

按照如上函数声明初始化一个 thread 对象后,即开启了一个新的线程。在使用 thread 创建线程进行并发编程时,需要注意以下几点:

  1. 在开启一个新的线程后,需要在恰当的位置调用 join 或 detach。调用 join 函数会使 调用线程 阻塞,直至被调用线程运行结束。调用 detach 函数会使调用线程和被调用线程分离。
  2. thread 对象不能显示地传递返回值给 调用线程,可以间接通过 promise 和 future 来实现。
  3. 当使用 thread 进行并发编程时,若线程执行过程中有异常产生,会直接终止程序。因此在使用 thread 进行并发编程时,需要在被调用线程中进行异常处理。

这里对上述注意事项中的第三点进行一个补充,代码示例如下:

void func()
{std::cout << "start func" << std::endl;// 运行过程中有异常产生,没有进行捕获throw std::runtime_error("runtime error");std::cout << "end func" << std::endl;
}int main()
{std::cout << "start main" << std::endl;// 尝试捕获异常,但是无效!try {std::thread t1(func);t1.join();     // 这样写是不对的,《Effective Modern C++》Item35 和 Item37 有解释} catch(const std::exception& e) {std::cout << e.what() << std::endl;}std::cout << "end main" << std::endl;
}/*
运行结果为:
start main
start func
terminate called after throwing an instance of 'std::runtime_error'what():  runtime error
Aborted
*/

thread 的其他 API 使用方法,文档中已有详细介绍,这里不再赘述。对于上述列的三点注意事项,展开说来又是一篇文章了。

std::mutex

使用 thread 开启一个新的线程非常简单,一行代码就搞定。接下来介绍互斥量 (mutex) 的基本使用。

在C++标准库中,提供了好几种互斥量类型,mutex、recursive_mutex、timed_mutex、recursive_timed_mutex,C++14增加了shared_timed_mutex,C++17增加了shared_mutex。本文只介绍 mutex 的基本使用。

mutex 是一种排他的互斥量,在并发环境中,进入临界区前先对互斥量进行加锁操作,临界区访问结束后对互斥量进行解锁操作。mutex 的使用也很简单,如下代码示例所示:

std::mutex mtx;   // 创建了一个互斥量// 进入临界区前先加锁,若加锁失败(当前线程之前,已有其他线程加锁),当前线程会被阻塞在该处
mtx.lock(); 
// 临界区
// ......
// 临界区
mtx.unlock();

如上示例所示,使用C++标准库提供的 mutex 非常方便。但是上述形式的用法可能存在以下两个问题,在并发编程中要尽量避免。

  1. 上述第8行的 mtx.unlock() 漏写,导致互斥量没被解锁,产生死锁现象。
  2. 临界区内有异常发生且未被正确捕获,则产生异常处之后的代码不会被执行,即 mtx.unlock() 不会被执行,产生死锁。

为避免上述两种的情况,C++标准库提供了非常方便的 mutex 管理类,lock_guard 和 unique_lock(基于C++11),C++14增加了shared_lock,C++17增加了scoped_lock。本文只介绍 unique_lock,若要全面介绍这四种 mutex 管理类及其使用场景,又是另一篇文章了。

使用基于 unique_lock 解决使用原始 mutex 可能产生的两个问题,代码示例如下:

std::mutex mtx;   // 创建了一个互斥量// 使用花括号限定 unique_lock 的作用域
{std::unique_lock<std::mutex> locker(mtx);// 临界区// ......// 临界区
}

unique_lock 类定义等价于如下代码:

class unique_lock {
public:explicit unique_lock(std::mutex& m):mtx(m) {mtx.lock();} unique_lock(const unique_lock&) = delete;~unique_lock() {mtx.unlock();}private:std::mutex& mtx;
};

因此,使用 unique_lock 来管理 mutex 是一种资源获取即初始化(Resource Acquisition Is Initialization;RAII)的思想。

std::condition_variable

在多线程环境中,线程的执行过程在某个时间段内可能存在先后关系,比如B线程运行到某个时刻点时,需要等待A线程的某个特定事件发生后才能继续往下执行,这种关系又称为同步。解决这种线程通信的问题的一种方案为 条件变量。

在C++标准库中,条件变量 std::condition_variable 的使用和 thread、mutex 一样简单,C++标准库提供了非常简洁的接口。接下来先来看看条件变量的基本用法长什么样,然后结合上述的面试题,来尝试总结如何使用条件变量解决线程间的同步关系。

条件变量的基本用法如下所示:

std::condition_variable cv;         //事件的条件变量
std::mutex mtx;                       //配合cv使用的mutex// 关键代码部分
{std::unique_lock<std::mutex> locker(mtx);cv.wait(mtx, [](){ /* 等待事件是否发生的条件判断 */ });// 对事件进行反应,执行相关操作。此时 mtx 已经上锁// ...// 可选的操作,通知一个或所有等待该事件的线程// cv.notify_one();// cv.notify_all();
}  // 退出该作用域,unique_lock执行析构函数,调用mtx.unlock()

以上述的面试题为例,看看 std::condition_variable 如何使用。简化的代码示例如下:

std::condition_variable cv; //事件的条件变量
std::mutex m; //配合cv使用的mutex// 用来控制事件变化的变量
int current_tid = 0;  void func(int tid)
{std::unique_lock<std::mutex> locker(mtx);cv.wait(locker, [=](){ return current_tid == tid; });// ...// 相关操作// ...current_tid = (current_tid + 1) % 2;   // 改变条件cv.notify_one();      // 唤醒阻塞在条件变量上的一个线程
}int main()
{std::thread t1(func, 0);std::thread t2(func, 1);// 省略一些代码...
}

解释一下上述代码:

  • 若执行 func 函数的线程被阻塞,则有可能有两种情况:
    1. 进入函数体,刚执行第9行语句时,mutex 因被其他线程先调用 mtx.lock() 而被阻塞;
    2. 进入函数体后,std::unique_lock<std::mutex> locker(mtx); 语句将 mtx 锁住之后,调用 cv.wait() 语句;因为cv.wait() 语句的第二个参数返回 false (在上面示例中,等价于 current_tid != tid),cv.wait() 该语句将当前线程阻塞,在阻塞前会调用 mtx.unlock() 释放互斥锁;然后当前被阻塞,等待其他线程调用 cv.notify_one() 或 cv.notify_all() 将该线程唤醒。
  • 若执行 func 函数的线程没被阻塞:
    线程顺利获取到 mutex ,然后调用 cv.wait() 语句,第二个参数返回 ture,逻辑流程继续往下执行,然后执行相关操作,然后改变 current_tid (控制事件变化的变量)值,然后调用 cv.notify_one() 唤醒阻塞在该条件变量上的线程。

小结:
使用条件变量控制线程之间的同步关系时,关键在如何将事件变化的关系抽象出来,用一个合适的变量(数据结构)来表示该事件的状态,通过改变变量的值(事件的状态)来控制线程之间的同步关系。

相关文章:

从一道面试题开始学习C++标准库提供的并发编程工具

一个空列表&#xff0c;用两个函数&#xff08;只可调用一次&#xff09;轮流写入值&#xff08;一个写奇数&#xff0c;一个写偶数&#xff09;&#xff0c; 最终实现列表的值为1-100&#xff0c;有序排列。 简单分析&#xff1a;假设这两个函数分别为A和B&#xff0c;A函数往…...

第三章 内存管理 十三、页面置换算法(最佳置换算法、先进先出置换算法、最近最久未使用置换算法、时钟置换算法、改进型的时钟置换算法)

目录 一、定义 二、分类 1、最佳置换算法 / 最远置换算法&#xff08;OPT&#xff0c;Optimal): 1.1、定义&#xff1a; 1.2、例子&#xff1a; 2、先进先出置换算法(FIFO&#xff09;: 2.1、定义&#xff1a; 2.2、实现方法&#xff1a; 2.3、例子&#xff1a; 3、最…...

连接到EC2,开启root登录

1.启动完新实例&#xff0c;下载密钥对密钥对登录 ssh -i "ec2-user.pem" ec2-userec2-xx-xx-xx-xx.compute-1.amazonaws.com2.为root设置密码 sudo passwd root3.切换到root权限 su root4.修改ssh配置文件&#xff0c;允许密码登陆 vi /etc/ssh/sshd_config Pas…...

线性代数-Python-02:矩阵的基本运算 - 手写Matrix及numpy中的用法

文章目录 一、代码仓库二、矩阵的基本运算2.1 矩阵的加法2.2 矩阵的数量乘法2.3 矩阵和向量的乘法2.4 矩阵和矩阵的乘法2.5 矩阵的转置 三、手写Matrix代码Matrix.pymain_matrix.pymain_numpy_matrix.py 一、代码仓库 https://github.com/Chufeng-Jiang/Python-Linear-Algebra-…...

6.MySQL内置函数

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 日期函数 current_date() 当前日期 select 可以做表达式和函数的计算。 current_time() 当前时间 current_timestamp() 当前日期加时间 注意&#xff1a;值得说明的是这三个函数底层调用的都是同一个函数&#xff0c;只不…...

3dmax中导出模型到unity注意事项

从3dmax中导出 1. 注意单位&#xff0c;根据需要&#xff0c;选英寸还是选厘米 2. 不能导出有错误的骨骼&#xff0c;否则导入后模型网格里出现 Skinned Mesh Renderer &#xff0c;对网格变换移动有影响&#xff0c;正常情况下都应该是 Mesh Renderer 3. 导出一般不带光源和…...

QTday05(TCP的服务端客户端通信)

实现聊天室功能 服务端代码&#xff1a; pro文件需要导入 network 头文件&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTcpServer>//服务端 #include <QTcpSocket>//客户端 #include <QList> #include <QMes…...

【MATLAB源码-第52期】基于matlab的4用户DS-CDMA误码率仿真,对比不同信道以及不同扩频码。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 1. DS-CDMA系统 DS-CDMA (Direct Sequence Code Division Multiple Access) 是一种多址接入技术&#xff0c;其基本思想是使用伪随机码序列来调制发送信号。DS-CDMA的特点是所有用户在同一频率上同时发送和接收信息&#xf…...

Spring 路径与占位符

SpringMVC支持ant风格的路径 &#xff1f;&#xff1a;表示任意的单个字符 *&#xff1a;表示任意的0个或多个字符 \**&#xff1a;表示任意的一层或多层目录 注意&#xff1a;在使用**时&#xff0c;只能使用/**/xxx的方式 1.测试 &#xff1f; <a th:href"{/succe…...

MIT 6.824 -- Cache Consistency -- 11

MIT 6.824 -- Cache Consistency -- 11 引言严峻挑战锁服务缓存一致性问题案例演示优化 原子性问题故障恢复问题log内容故障恢复 小结 课程b站视频地址: MIT 6.824 Distributed Systems Spring 2020 分布式系统 推荐伴读读物: 极客时间 – 大数据经典论文解读DDIA – 数据密集…...

Python在列表中如何对多个参数进行修改

1 问题 在python中经常会使用到列表&#xff0c;列表是常见的一种数据类型。对于一个庞大的列表&#xff0c;要调取列表中的对象&#xff0c;应如何快速准确的调取或快速的调取多个对象&#xff1f; 2 方法 解决问题的步骤采用如下方式&#xff1a; 基本的&#xff0c;已知元素…...

手机启用adb无线调试

具体步骤 手机和电脑处于同一个路由器下。 比如手机IP是192.168.31.181&#xff0c;电脑能ping通。 手机端启用无线adb调试先把手机用USB线连接电脑&#xff0c;打开adb&#xff0c;输入以下命令&#xff1a; G:\> adb tcpip 5555 restarting in TCP mode port: 5555 无…...

openGauss学习笔记-105 openGauss 数据库管理-管理用户及权限-默认权限机制

文章目录 openGauss学习笔记-105 openGauss 数据库管理-管理用户及权限-默认权限机制 openGauss学习笔记-105 openGauss 数据库管理-管理用户及权限-默认权限机制 数据库对象创建后&#xff0c;进行对象创建的用户就是该对象的所有者。openGauss安装后的默认情况下&#xff0c…...

[翻译]理解Postgres的IOPS:为什么数据即使都在内存,IOPS也非常重要

理解Postgres的IOPS&#xff1a;为什么数据即使都在内存&#xff0c;IOPS也非常重要 磁盘IOPS&#xff08;每秒输入/输出操作数&#xff09;是衡量磁盘系统性能的关键指标。代表每秒可以执行的读写操作数量。对于严重依赖于磁盘访问的PG来说&#xff0c;了解和优化磁盘IOPS对实…...

Day6力扣打卡

打卡记录 统计无向图中无法互相到达点对数&#xff08;并查集 / DFS&#xff09; 链接 并查集 思路&#xff1a;用并查集将连通区域的连在一起&#xff0c;再遍历所有点&#xff0c;用hash表存储不同连通块的元素个数&#xff0c;然后 乘积和 便是答案。 注意&#xff1a; /…...

10月面试js基础

作用域 变量的可用范围 作用域链 保存的变量的使用顺序的一个链&#xff08;也就是路线图&#xff09;&#xff0c; 被称为作用域链。 当在Javascript中使用一个变量的时候&#xff0c;首先Javascript引擎会尝试在当前作用域下去寻找该变量&#xff0c;如果没找到&#xff0c;再…...

研发日常踩坑-Mysql分页数据重复 | 京东云技术团队

踩坑描述: 写分页查询接口&#xff0c;order by和limit混用的时候&#xff0c;出现了排序的混乱情况 在进行第N页查询时&#xff0c;出现与第一前面页码的数据一样的记录。 问题 在MySQL中分页查询&#xff0c;我们经常会用limit&#xff0c;如:limit(0,20)表示查询第一页的…...

Ubuntu18.04安装QGC报错 `GLIBC_2.29‘ not found

按照官网教程&#xff0c;最后运行时出错。 /tmp/.mount_QGroun2NOhPP/QGroundControl: /lib/x86_64-linux-gnu/libm.so.6: version GLIBC_2.29 not found (required by /tmp/.mount_QGroun2NOhPP/QGroundControl) /tmp/.mount_QGroun2NOhPP/QGroundControl: /usr/lib/x86_64-…...

回归预测 | MATLAB实现BO-GRU贝叶斯优化门控循环单元多输入单输出回归预测

回归预测 | MATLAB实现BO-GRU贝叶斯优化门控循环单元多输入单输出回归预测 目录 回归预测 | MATLAB实现BO-GRU贝叶斯优化门控循环单元多输入单输出回归预测效果一览基本介绍模型搭建程序设计参考资料 效果一览 基本介绍 MATLAB实现BO-GRU贝叶斯优化门控循环单元回归预测。基于贝…...

Easyx趣味编程7,鼠标消息读取及音频播放

hello大家好&#xff0c;这里是dark flame master&#xff0c;今天给大家带来Easyx图形库最后一节功能实现的介绍&#xff0c;前边介绍了绘制各种图形及键盘交互&#xff0c;文字&#xff0c;图片等操作&#xff0c;今天就可以使写出的程序更加生动且容易操控。一起学习吧&…...

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…...

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...

挑战杯推荐项目

“人工智能”创意赛 - 智能艺术创作助手&#xff1a;借助大模型技术&#xff0c;开发能根据用户输入的主题、风格等要求&#xff0c;生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用&#xff0c;帮助艺术家和创意爱好者激发创意、提高创作效率。 ​ - 个性化梦境…...

idea大量爆红问题解决

问题描述 在学习和工作中&#xff0c;idea是程序员不可缺少的一个工具&#xff0c;但是突然在有些时候就会出现大量爆红的问题&#xff0c;发现无法跳转&#xff0c;无论是关机重启或者是替换root都无法解决 就是如上所展示的问题&#xff0c;但是程序依然可以启动。 问题解决…...

React Native 开发环境搭建(全平台详解)

React Native 开发环境搭建&#xff08;全平台详解&#xff09; 在开始使用 React Native 开发移动应用之前&#xff0c;正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南&#xff0c;涵盖 macOS 和 Windows 平台的配置步骤&#xff0c;如何在 Android 和 iOS…...

python/java环境配置

环境变量放一起 python&#xff1a; 1.首先下载Python Python下载地址&#xff1a;Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个&#xff0c;然后自定义&#xff0c;全选 可以把前4个选上 3.环境配置 1&#xff09;搜高级系统设置 2…...

将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?

Otsu 是一种自动阈值化方法&#xff0c;用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理&#xff0c;能够自动确定一个阈值&#xff0c;将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...

2021-03-15 iview一些问题

1.iview 在使用tree组件时&#xff0c;发现没有set类的方法&#xff0c;只有get&#xff0c;那么要改变tree值&#xff0c;只能遍历treeData&#xff0c;递归修改treeData的checked&#xff0c;发现无法更改&#xff0c;原因在于check模式下&#xff0c;子元素的勾选状态跟父节…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)

引言&#xff1a;为什么 Eureka 依然是存量系统的核心&#xff1f; 尽管 Nacos 等新注册中心崛起&#xff0c;但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制&#xff0c;是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南

&#x1f680; C extern 关键字深度解析&#xff1a;跨文件编程的终极指南 &#x1f4c5; 更新时间&#xff1a;2025年6月5日 &#x1f3f7;️ 标签&#xff1a;C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言&#x1f525;一、extern 是什么&#xff1f;&…...