C++并发编程(一):线程基础
简介
本文学习的是 b 站 up 恋恋风辰的并发编程教学视频做的一些笔记补充。
教程视频链接如下:线程基础:视频教程
文档链接如下:线程基础:笔记文档
理论上直接看 up 提供的笔记文档即可,我这里主要是记录一些我自己觉得可能需要补充的点。
线程发起
void thread_work1(std::string str) {std::cout << "str is " << str << std::endl;
}
//1 通过()初始化并启动一个线程
std::thread t1(thead_work1, hellostr);
解释一下这行代码:
std::thread t1(thread_work1, hellostr);
1、创建 std::thread 对象:t1 是一个 std::thread 类型的对象,它代表了一个即将或正在执行的线程。
2、指定线程要执行的任务:通过构造函数,我们告诉 t1 线程应该执行哪个函数。在这个例子中,任务是调用 thread_work1 函数。
3、传递参数给任务:thread_work1 函数需要一个 std::string 类型的参数。在构造 t1 时,我们通过传递 hellostr 变量来提供这个参数。hellostr 是一个已经定义并初始化的 std::string 对象,其值将被传递给 thread_work1 函数。
4、启动线程:当 std::thread 对象被构造时,它会自动启动一个新线程来执行指定的任务(即调用 thread_work1 函数,并将 hellostr 作为参数传递)。这个过程是异步的,意味着主线程(即创建 t1 的线程)将继续执行其后续指令,而不会等待新线程完成。
5、线程管理:一旦线程被启动,它就在自己的执行上下文中独立运行。但是,std::thread 对象 t1 提供了管理这个线程的手段,比如通过调用 t1.join() 来等待线程完成,或者通过调用 t1.detach() 来分离线程(让它在后台运行,而 t1 对象不再管理它)。
注意:普通函数的函数名就是这个函数实际的地址。
我们可以来试验一下这个事情:
#include <iostream>
#include <string>
#include <thread> # 注意使用多线程时要包含这个头文件using namespace std;//线程函数
void thread_work1(std::string str) {std::cout << "str is " << str << std::endl;
}int main() {string hellostr = "hello world";//通过 () 初始化并启动一个线程thread t1(thread_work1, hellostr);return 0;
}

可以看见运行结果是程序崩溃了,为什么呢?
当我们定义完线程 t1 之后它就会自动初始化并开始运行,那么在后台就会开始执行 thread_work1 这个线程函数然后输出 hello world。但是程序此时继续往下走时我们并没有将这个线程挂起或者停靠结果主线程就结束了,主线程结束就必须要回收 hellostr 这个字符串资源,那么就可能会存在这个资源已经被释放了,那么在 thread_work1 里虽然依然能够调用资源但是有可能会出问题(只是当前例子没出)。
因为在代码中我们可以看到传参是通过值传递,也就是把外部的局部变量作为一个拷贝拷贝给线程函数 thread_work1 ,所以在这个例子中字符串依然可以正常输出,那为什么会崩溃?就是因为主线程退出了,而子线程则有可能是还在运行的,那么就会崩溃。
但要注意,即使我们能够人为的保证让主线程在子线程执行完之后再结束主线程(比如让主线程睡上几秒)上述的崩溃问题也依然存在。
这是因为11新标准的这套线程 API 做了优化,当编译器发现我们启动了一个线程但却没有把这个线程做善后的工作(比如join或者detach掉),那么就会出现主线程在回收资源的时候就会调用这个线程的析构函数,析构函数内部就会执行一个很生硬的 terminate 函数,这个函数就会强制终止,这个函数的强制终止是会调用 assert 断言导致崩溃的。
线程等待
因此为了防止这样的崩溃问题,我们可以加入一个 join 函数:
int main() {string hellostr = "hello world";//通过 () 初始化并启动一个线程thread t1(thread_work1, hellostr);//主线程等待子线程退出t1.join();return 0;
}
这个 t1.join 会让主线程去等待子线程 t1 执行完了主线程再继续往下执行。
此时再运行将不会发生问题。
仿函数(函数对象)作为参数
当我们用仿函数作为参数传递给线程时,也可以达到启动线程执行某种操作的含义。
但是要注意一些问题,我们可以来看个例子:
class background_task {
public:// 实现了括号运算符的类就称可以创建函数对象void operator()() {cout << "background_task called" << endl;}
};int main() {//t2 被当作函数对象的定义,其类型为返回 thread,参数为 background_taskthread t2(background_task());//t2.join(); 编译错误return 0;
}
线程对象 t2 会去执行 background_task 这个类的函数对象,当我们使用 background_task() 的时候会调用这个类的构造函数,结果是会生成一个对象,这个对象传递给了这个线程 t2 作为参数,同时因为我们重载了括号运算符,所以这个对象可以直接执行,也就是可以把对象当成函数来使用,这也是函数对象的意义。
但是可以看到在调用 join 函数时,出现编译错误:

这是因为编译器会将 thread t2(background_task()); 这行代码解释成了一个函数指针, 返回一个 std::thread 类型的值, 这个函数指针的参数也为一个函数指针, 这个函数指针返回值则为background_task, 参数为void。可以理解为如下:
"std::thread (*)(background_task (*)())"
这样看有点不太好懂,因为我们说编译器会将 t2 当成一个函数指针,不妨拆解一下上面这行代码:
std::thread 是返回值,(*) 是一个函数指针,()是参数列表,最后 background_task (*)() 是该函数指针的参数,也是一个函数指针。
再对比一下函数指针的声明形式:
返回类型 (*指针变量名)(参数类型列表);
这样应该就好明白多了。
我们明明是想定义一个线程对象 t2,却被编译器解释成了一个函数指针,这肯定是不行的。
解决方案如下:
//可多加一层()
//此时编译器就会认为其是一个对象,进行正常的线程初始化并启动了
std::thread t2((background_task()));
t2.join();//可使用{}方式进行变量初始化
std::thread t3{ background_task() };
t3.join();
此时编译就正常了。
lambda表达式
lambda 表达式也可以作为线程的参数传递给thread:
std::thread t4([](std::string str) { std::cout << "str is " << str << std::endl;}, hellostr);t4.join();
线程detach
线程允许采用分离的方式在后台独自运行,C++ concurrent programing书中称其为守护线程。
方式是使用 detach 函数,该函数会让线程在后台运行,它不会受主线程的影响,主线程可以直接执行自己的任务,子线程就和主线程分离了,它们会各自使用自己的资源。
但是这里要注意:分离的时候,一旦子线程需要用到主线程的资源时,由于主线程运行结束,资源释放了,那么子线程在获取主线程资源时就容易产生问题,来看下面的例子。
struct func {//对于类和结构体的成员属性是引用的话,那么可以在构造函数初始化列表中初始化int& _i;func(int& i) : _i(i) {}void operator()() {for (int i = 0; i < 3; i++) {_i = i;std::cout << "_i is " << _i << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));}}
};void oops() {int some_local_state = 0;//使用函数对象创建 func 类的对象并调用函数func myfunc(some_local_state);//创建并启动线程std::thread functhread(myfunc);//演示隐患,子线程还在访问局部变量 some_local_state,但局部变量可能会随着 } 结束而回收或随着主线程退出而回收functhread.detach();
}int main() {// detach 注意事项oops();//防止主线程退出过快,需要停顿一下,让子线程跑起来detachstd::this_thread::sleep_for(std::chrono::seconds(1));return 0;
}
主线程在执行完 oops() 后又睡了一秒然后就退出了。
虽然主线程退出了,但是子线程还在执行。不过这里要注意,因为主线程就是进程存在的主要形式,进程包括主线程以及其衍生的一众子线程,因此主线程如果结束了那么整个进程也就结束了,那自然而然所有的子线程即使是在 detach 的状态也会被操作系统给全部回收掉。
上面的例子存在隐患,因为some_local_state是局部变量, 当oops调用结束后局部变量some_local_state就可能被释放了,而线程还在detach后台运行,容易出现崩溃。
所以当我们在线程中使用局部变量的时候可以采取几个措施解决局部变量的问题
1、通过智能指针传递参数,因为引用计数会随着赋值增加,可保证局部变量在使用期间不被释放,这也就是我们之前提到的伪闭包策略。
2、将局部变量的值作为参数传递,这么做需要局部变量有拷贝复制的功能,而且拷贝耗费空间和效率。
3、将线程运行的方式修改为join,这样能保证局部变量被释放前线程已经运行结束。但是这么做可能会影响运行逻辑。
比如下面的修改:
void use_join() {int some_local_state = 0;func myfunc(some_local_state);std::thread functhread(myfunc);functhread.join();
}// join 用法
use_join();
异常处理
当我们启动一个线程后,如果主线程产生崩溃,会导致子线程也会异常退出(因为主进程就是依赖于主线程存在的),也就是之前说的调用terminate。如果子线程在进行一些重要的操作比如将充值信息入库等,那么丢失这些信息是很危险的。所以常用的做法是捕获异常,并且在异常情况下保证子线程稳定运行结束后,主线程再抛出异常结束运行。如下面的逻辑:
void catch_exception() {int some_local_state = 0;func myfunc(some_local_state);std::thread functhread{ myfunc };try {//本线程做一些事情,假设可能引发崩溃std::this_thread::sleep_for(std::chrono::seconds(1));}catch (std::exception& e) {//一旦引发崩溃,那么就会在这里被捕获住//捕获到异常后主线程不能马上崩溃,要先等子线程运行结束functhread.join();//子线程运行结束后,此时主线程再来处理该异常throw;}//如果没有异常那就正常继续下去即可functhread.join();
}
但是用这种方式编码,会显得臃肿,可以采用 RAII 技术,保证线程对象析构的时候等待线程运行结束,回收资源。
介绍一下 RAII 技术:

那么我们就来写一个简单的线程守卫:
class thread_guard {
private:std::thread& _t;
public:explicit thread_guard(std::thread& t):_t(t){}~thread_guard() {//join只能调用一次if (_t.joinable()) {_t.join();}}thread_guard(thread_guard const&) = delete;thread_guard& operator=(thread_guard const&) = delete;
};
可以这么使用:
void auto_guard() {int some_local_state = 0;func my_func(some_local_state);std::thread t(my_func);thread_guard g(t);//本线程做一些事情std::cout << "auto guard finished " << std::endl;
}
auto_guard();
慎用隐式转换
C++中会有一些隐式转换,比如 char* 转换为 string 等。这些隐式转换在线程的调用上可能会造成崩溃问题:
void danger_oops(int som_param) {char buffer[1024];sprintf(buffer, "%i", som_param);//在线程内部将char const* 转化为std::stringstd::thread t(print_str, 3, buffer);t.detach();std::cout << "danger oops finished " << std::endl;
}
当我们定义一个线程变量thread t时,传递给这个线程的参数buffer会被保存到thread的成员变量中。
而在线程对象t内部启动并运行线程时,参数才会被传递给调用函数print_str。
而此时 buffer 可能随着 } 运行结束而释放了。
改进的方式很简单,我们将参数传递给thread时显示转换为string就可以了,这样thread内部保存的是string类型。
void safe_oops(int some_param) {char buffer[1024];sprintf(buffer, "%i", some_param);std::thread t(print_str, 3, std::string(buffer));t.detach();
}
关于为什么参数会像我说的这样保存和调用,我在之后会按照源码给大家讲一讲。
引用参数
当线程要调用的回调函数参数为引用类型时,需要将参数显示转化为引用对象传递给线程的构造函数,如果采用如下调用会编译失败:
void change_param(int& param) {param++;
}
void ref_oops(int some_param) {std::cout << "before change , param is " << some_param << std::endl;//需使用引用显示转换std::thread t2(change_param, some_param);t2.join();std::cout << "after change , param is " << some_param << std::endl;
}
即使函数 change_param 的参数为 int& 类型,我们传递给 t2 的构造函数为 some_param, 也不会达到在change_param 函数内部修改关联到外部some_param的效果。因为 some_param 在传递给 thread 的构造函数后会转变为右值保存,右值传递给一个左值引用会出问题,所以编译出了问题。
改为如下调用就可以了:
void ref_oops(int some_param) {std::cout << "before change , param is " << some_param << std::endl;//需使用引用显示转换std::thread t2(change_param, std::ref(some_param));t2.join();std::cout << "after change , param is " << some_param << std::endl;
}
绑定类成员函数
有时候我们需要绑定一个类的成员函数:
class X
{
public:void do_lengthy_work() {std::cout << "do_lengthy_work " << std::endl;}
};
void bind_class_oops() {X my_x;std::thread t(&X::do_lengthy_work, &my_x);t.join();
}
这里大家注意一下,如果thread绑定的回调函数是普通函数,可以在函数前加 & 或者不加 & ,因为编译器默认将普通函数名作为函数地址,如下两种写法都正确:
void thead_work1(std::string str) {std::cout << "str is " << str << std::endl;
}
std::string hellostr = "hello world!";
//两种方式都正确
std::thread t1(thead_work1, hellostr);
std::thread t2(&thead_work1, hellostr);
但是如果是绑定类的成员函数,必须添加 & 。
使用move操作
有时候传递给线程的参数是独占的,所谓独占就是不支持拷贝赋值和构造,但是我们可以通过std::move的方式将参数的所有权转移给线程,如下:
void deal_unique(std::unique_ptr<int> p) {std::cout << "unique ptr data is " << *p << std::endl;(*p)++;std::cout << "after unique ptr data is " << *p << std::endl;
}
void move_oops() {auto p = std::make_unique<int>(100);std::thread t(deal_unique, std::move(p));t.join();//不能再使用p了,p已经被move废弃// std::cout << "after unique ptr data is " << *p << std::endl;
}
相关文章:
C++并发编程(一):线程基础
简介 本文学习的是 b 站 up 恋恋风辰的并发编程教学视频做的一些笔记补充。 教程视频链接如下:线程基础:视频教程 文档链接如下:线程基础:笔记文档 理论上直接看 up 提供的笔记文档即可,我这里主要是记录一些我自己…...
enq: HW - contention事件来啦
业务系统反应数据库慢,根据时间查看awr报告。 先看一眼事件名称 HW enqueue 用于序列化超出段高水位线的空间分配。如果同时向对象添加大量数据,则多个进程可能同时尝试在高水位线上方分配空间,从而导致争用。 既然是控制资源并发的enq&…...
MyBatis补充
控制类和dao层接口以及mapper中的xml是怎样的关联的? 在Mybatis中,控制类和dao层接口是通过mapper的xml文件进行连接的。 控制类调用dao层接口中的方法,通过接口实现进行访问数据库操作。dao层接口定义数据库操作的方法,提供给控制…...
系统架构师(每日一练16)
每日一练 答案与解析 1.软件测试一般分为两个大类:动态测试和静态测试。前者通过运行程序发现错误,包括()等方法;后者采用人工和计算机辅助静态分析的手段对程序进行检测,包括()等方法。答案与解析 问题1 A.边界值分析、逻辑覆盖、基本路径 B.桌面检查、…...
实践致知第17享:电脑忽然黑屏的常见原因及处理方法
一、背景需求 小姑电话说:最近,电脑忽然就黑屏了(如下图所示),但是等待几十秒甚至一分钟,电脑就能自然恢复了,这种状况一天能出现三四次,怎么办? 二、分析诊断 电脑黑屏…...
微信小程序--实现地图定位---获取经纬度
(1) (2) (3) html: <view class"titleTwo" style"border: none;"><view class"fontSize30 invoiceTile">企业地址</view><view class"invoiceRight" bind:tap"tapChooseAddress" data-maptype"…...
【Python系列】使用 `isinstance()` 替代 `type()` 函数
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...
【多模态大模型】 BLIP-2 in ICML 2023
一、引言 论文: BLIP-2: Bootstrapping Language-Image Pre-training with Frozen Image Encoders and Large Language Models 作者: Salesforce Research 代码: BLIP-2 特点: 该方法分别使用冻结的图像编码器(ViT-L/…...
HPC高性能计算平台
随着技术的发展和数据量的爆炸性增长,企业面临的挑战日益复杂,对计算能力的需求也在不断增加。这些问题的解决超出了传统计算方法的能力范围,高性能计算(HPC)正是为解决这类问题而生。 高性能计算(HPC&…...
前端常用的几个工具网站
觉得不错的前端工具类网站 1、Grid布局生成 https://cssgrid-generator.netlify.app 2、拟物按钮样式生成 https://neumorphism.io 3、玻璃形态效果 在线制作CSS玻璃形态 4、一些Button、checkBox、switch、card的css样式 零代码 - 精美CSS样式库 5、CSS阴影生成 在线创建…...
支付功能之代收代付
有很多老板问小编:“这个分账功能好是好,也能搞定项目中的二清问题和税务纠纷,但还是太复杂了,每次要添加被分账对象都需要提交材料进行审核,太繁琐了,有没有更方便快捷的支付产品来解决资金问题࿱…...
QPixmap
pixel[ˈpɪksl]像素 QPixmap 是 Qt 框架中用于处理图像的一个类。它主要用于在屏幕上显示和处理图像,提供了许多实用的功能,如加载、保存、缩放、旋转、合并等。 绘制 从文件加载:从指定文件加载图像。 QPixmap pixmap(":/images/exam…...
Laravel门面之下:构建自定义门面应用的艺术
Laravel门面之下:构建自定义门面应用的艺术 在Laravel框架中,门面(Facade)提供了一种将类静态调用与面向对象代码解耦的优雅方式。门面是一个全局可访问的类,它为底层复杂的服务提供了一个简单的接口。然而࿰…...
智启万象 | 2024 Google 开发者大会直播攻略
8 月 7 日上午 9:30 2024 Google 开发者大会 主旨演讲直播将准时开启 想要在线上探索大会精彩内容? 快查收这份观看指南! 8 月 7 日上午 9:30 2024 Google 开发者大会开幕 锁定大会官网观看主旨演讲现场直播! 本次大会内容将同步于多个…...
技巧:print打印内容到控制台时信息显示不全
# 请求一个接口,res是响应内容,使用res.text打印的信息不全 #使用流式处理响应 #如果你需要流式处理大的响应,确保你在处理响应内容的同时不会提前结束流。resself.request_base(select_api,change_datachange_data)print("")# pri…...
3.表的操作
目录 创建表 创建表案例: 查看表结构 修改表 1.增加新列 2.修改列的属性 3.删除列 4.修改表名 5.修改列 删除表 创建表 语法: CREATE TABLE [IF NOT EXISTS] table_name(field1 datatype1 [COMMENT 注释信息],field2 datatype2 [COMMENT 注释…...
AI回答:C#项目编译后生成部分文件的主要职责
【引入】以ConsoleApp1为例,请问C#编译之后以下文件有啥用 1.bin\runtimes 文件夹存放什么,有什么用? bin\runtimes 文件夹存放了项目的运行时相关文件,这些文件包括了各种目标平台的运行时库。 2.bin\生成的exe文件可以在别的电脑…...
RPC通信的简单流程
远程调用者假设需要调用Login方法,将调用的信息通过muduo库,同时进行了序列化和反序列化,发送到Rpcprovider上,RpcProvider通过对象和方法表来确定需要调用哪个服务对象的哪个方法。 UserRpcServiceRpc和UseRpcServiceRpcStub是继…...
前端发版(发包)缓存,需要强制刷新问题处理
问题原因: 浏览器问题 一、创建初始版本文件(public/version.json) { "version": "1722240835844" }二、设置版本判断(version.js) import axios from "axios";const isNewVersion () > {let baseUrl …...
洛谷练习(8.4/8.5)
题目 P2036 PERKET题目描述思路代码 P3799 小 Y 拼木棒题目描述思路代码 P1010 幂次方题目描述思路代码 P1498 南蛮图腾题目描述思路代码 P1928 外星密码题目描述思路代码 P2036 PERKET 题目描述 比较苦度和酸度的最小差值 思路 搜索最小差值 代码 void dfs(int sd,int k…...
网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
idea大量爆红问题解决
问题描述 在学习和工作中,idea是程序员不可缺少的一个工具,但是突然在有些时候就会出现大量爆红的问题,发现无法跳转,无论是关机重启或者是替换root都无法解决 就是如上所展示的问题,但是程序依然可以启动。 问题解决…...
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...
Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具
文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...
将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?
Otsu 是一种自动阈值化方法,用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理,能够自动确定一个阈值,将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...
Module Federation 和 Native Federation 的比较
前言 Module Federation 是 Webpack 5 引入的微前端架构方案,允许不同独立构建的应用在运行时动态共享模块。 Native Federation 是 Angular 官方基于 Module Federation 理念实现的专为 Angular 优化的微前端方案。 概念解析 Module Federation (模块联邦) Modul…...
