从一道面试题开始学习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() 唤醒阻塞在该条件变量上的线程。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/111024.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Sqoop技术文档笔记

Sqoop是一个用于在Hadoop和关系型数据库之间传输数据的开源工具。它可以将结构化数据从关系型数据库&#xff08;如MySQL、Oracle、SQL Server等&#xff09;导入到Hadoop的分布式文件系统&#xff08;HDFS&#xff09;或hive中&#xff0c;并且可以将数据从HDFS、hive导出到关…

安装VSCode,提升工作效率!iPad Pro生产力进阶之路

文章目录 前言1. 本地环境配置2. 内网穿透2.1 安装cpolar内网穿透(支持一键自动安装脚本)2.2 创建HTTP隧道 3. 测试远程访问4. 配置固定二级子域名4.1 保留二级子域名4.2 配置二级子域名 5. 测试使用固定二级子域名远程访问6. iPad通过软件远程vscode6.1 创建TCP隧道 7. ipad远…

mac 启动mysql Error: Failure while executing; `/bin/launchctl bootstrap gui/501

Error: Failure while executing; /bin/launchctl bootstrap gui/501 /Users/<myUserName>/Library/LaunchAgents/homebrew.mxcl.mysql8.0.plist exited with 5.homebrew 给的提示看不到具体消息 查看 homebrew.mxcl.mysql8.0.plist文件&#xff0c;能看到具体的启动命令…

Netty使用SslHandler实现加密通信-双向认证篇

“不积跬步&#xff0c;无以至千里。” 说明 其实Netty使用SslHandler实现加密通信单向认证和双向认证在代码上区别不大&#xff0c;下面是双向认证的代码示例 引入依赖 <dependency><groupId>io.netty</groupId><artifactId>netty-all</artifac…

webrtc基于DTLS的端口复用技术

DTLS协议: DTLS(Datagram Transport Layer Security)数据包安全传输协议,用于在不可靠的数据包传输协议上(如UDP)提供数据的安全传输。 UDP多路复用: 一个UDP多路复用&#xff0c;被用来处理共享同一个UDP端口的多个并发的UDT连接。类似同一个tcp port上创建多个socket connec…

【复盘】主从延迟以及 Waiting for tablemetadata lock 线上问题

背景 今晚DBA给一个大表添加索引&#xff0c;1000多W&#xff0c;正好风控系统这个时间段有查询这个表的请求&#xff0c;于是就出现了复制延迟。 这是正常下的延迟 可以看出基本都是是100毫秒以下。 Waiting for tablemetadata lock&#xff0c;并且业务跑的SQL出现锁等待…

append_ocr_trainf

read_image (Image, D:/图像文件/字符识别/1-1.bmp) access_channel (Image, Image1, 1) * draw_rectangle2 (3600, Row, Column, Phi, Length1, Length2) gen_rectangle2 (Rectangle, 96.0436, 715.9526, 0.0173917050943654, 110.186941, 18.041084) reduce_domain (Image1, …

多线程处理文件集合,先拆分,在执行

try {File file new File(path);File[] files file.listFiles();log.info("当前共有文件 "files.length"个");List<File> filesList new ArrayList<>(Arrays.asList(files));List<List<File>> dividedLists SplitListUtils.sp…

[笔记] 十进制转n进制

思路 n对 xa取模&#xff0c;就是xa-1 位上的数字&#xff0c;因为模出来的数不足xa 举例来说就是5&211&#xff0c;这个1就是20位上的1 当前位取完后&#xff0c;n/xa&#xff0c;表示n将对x(a1)进行取模&#xff08;进入下一位&#xff09; 重复此操作直至n0。 代码实现 …

开发者职场“生存状态”大调研报告分析 - 第四版

听人劝、吃饱饭,奉劝各位小伙伴,不要订阅该文所属专栏。 作者:不渴望力量的哈士奇(哈哥),十余年工作经验, 跨域学习者,从事过全栈研发、产品经理等工作,现任研发部门 CTO 。荣誉:2022年度博客之星Top4、博客专家认证、全栈领域优质创作者、新星计划导师,“星荐官共赢计…

.mxdown-V-XXXXXXXX勒索病毒感染后的下一步:恢复您的文件

引言&#xff1a; 在数字时代&#xff0c;计算机用户日益面临着来自网络犯罪分子的各种威胁&#xff0c;其中包括勒索病毒&#xff0c;如.mxdown-V-XXXXXXXX。这种勒索病毒可以对你的个人和商业数据文件进行加密&#xff0c;并要求支付赎金才能解锁它们。本文91数据恢复将介绍…

矩阵键盘中断扫描

/*----------------------------------------------- 内容&#xff1a;如计算器输入数据形式相同 从右至左 使用行列扫描方法 中断方式可以有效提供cpu工作效率&#xff0c;在有按键动作时才扫描&#xff0c;平时不进行扫描工作 -------------------------------------…

centos 7.9每天定期发送最新备份文件到另外一台服务器

1.需求 在本地化部署的过程中&#xff0c;为了使系统相对来说高可用&#xff0c;一般情况下&#xff0c;我们都会做一个负载&#xff0c;但是客户又会考虑成本&#xff0c;所以只有可怜巴巴的两台服务器&#xff0c;要全部服务都做负载&#xff0c;这个就实现不了。所以只能把…

1.13.C++项目:仿muduo库实现并发服务器之TcpServer模块的设计

文章目录 一、LoopThreadPool模块二、实现思想&#xff08;一&#xff09;管理&#xff08;二&#xff09;流程&#xff08;三&#xff09;功能设计 三、代码 一、LoopThreadPool模块 TcpServer模块&#xff1a; 对所有模块的整合&#xff0c;通过 tcpserver 模块实例化的对象&…

Python中的元组

Python 元组 Python 的元组与列表类似&#xff0c;不同之处在于元组的元素不能修改。以下是关于Python元组的一些基本信息&#xff1a; 元组的使用&#xff1a;元组是一个不可变的序列类型&#xff0c;使用小括号 () 来定义。元组没有增加元素append、修改元素、删除元素pop的…

【ListCtrl可以显示一部分吗】2023/10/14 下午1:38:38

2023/10/14 下午1:38:38 ListCtrl可以显示一部分吗 2023/10/14 下午1:38:45 ListCtrl是一个控件库,它可以在窗口中显示可滚动的列表。您可以使用SetItemCount方法设置要显示的项数,但在视图上只有部分项会显示,用户可以滚动以查看其他项。如果您想要指定要显示的具体项,…

Linux高性能服务器编程——ch2笔记

第2章 IP 协议详解 2.1 IP服务的特点 无状态&#xff1a;IP通信双方不同步传输数据的状态信息。IP数据报相互独立&#xff0c;缺点是无法处理乱序和重复的IP数据报。上层协议如果是面向连接的协议&#xff08;TCP&#xff09;&#xff0c;能够自己处理乱序和重复的报文段。IP…

【广州华锐互动】利用AR进行野外地质调查学习,培养学生实践能力

在科技发展的驱动下&#xff0c;AR&#xff08;增强现实&#xff09;技术已经在许多领域中找到了应用&#xff0c;包括医疗、教育、建筑和娱乐等。然而&#xff0c;有一个领域尚未充分利用AR技术的潜力&#xff0c;那就是野外地质调查。通过将AR技术引入到这个传统上需要大量人…

想找就能找!如何找回iPhone中被隐藏或主屏幕上被删除的应用程序

本文介绍了如何取消隐藏你在iPhone上隐藏的应用程序&#xff0c;以及如何检索你从iPhone中删除的应用程序。 如何取消隐藏隐藏的应用程序 你过去可能在iPhone上隐藏了应用程序&#xff0c;因为你不经常使用它们&#xff0c;或者你只是喜欢几个整洁的主屏幕。如果你决定将隐藏…

Write-Ahead Log(PostgreSQL 14 Internals翻译版)

日志 如果发生停电、操作系统错误或数据库服务器崩溃等故障&#xff0c;RAM中的所有内容都将丢失&#xff1b;只有写入磁盘的数据才会被保留。要在故障后启动服务器&#xff0c;必须恢复数据一致性。如果磁盘本身已损坏&#xff0c;则必须通过备份恢复来解决相同的问题。 理论…