C++并发编程(一):线程基础

news2024/11/16 4:42:04

简介

本文学习的是 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_task
	thread 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();
	//防止主线程退出过快,需要停顿一下,让子线程跑起来detach
	std::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::string
    std::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;
}

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

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

相关文章

免费qq号码估价的工具和软件

目前有多种 qq 号码估价的工具和软件。例如&#xff0c;晒号网的 QQ 估价器可以根据 QQ 号码等级、QQ 号码资深度、QQ 号码年限、活跃时间等进行准确的 QQ 号码估价。此外&#xff0c;还有其他一些相关的估价软件和平台&#xff0c;如 QQ 号码估价 2.0 全新玩法&#xff0c;利用…

复现opendrivelab的“点云预测”项目

本文的主要工作就是复现下述论文中的算法。 该论文全称&#xff1a;Visual Point Cloud Forecasting 论文内容在此不做过多介绍&#xff0c;直接上项目。 一、准备工作 首先通读readme.md文件的内容&#xff0c;了解所需要的相关依赖和数据等内容。 一定要多读几遍&#xf…

C# udp通信测试助手

1、UI界面和最终实现功能测试 2、代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threa…

【C++BFS算法】2998. 使 X 和 Y 相等的最少操作次数

本文涉及知识点 CBFS算法 LeetCode2998. 使 X 和 Y 相等的最少操作次数 给你两个正整数 x 和 y 。 一次操作中&#xff0c;你可以执行以下四种操作之一&#xff1a; 如果 x 是 11 的倍数&#xff0c;将 x 除以 11 。 如果 x 是 5 的倍数&#xff0c;将 x 除以 5 。 将 x 减 …

Linux项目实战——业务系统(后附百问网课程视频链接)

处理配置文件 一、main函数流程 初始化显示系统——>初始化输入系统——>初始化文字系统——>初始化页面系统——>业务系统 二、业务系统框架 1. 读取配置文件 2. 根据配置文件生成按钮、界面 3. 读取输入事件 4. 根据输入事件找到按钮 5. 调用按键的OnPressed函…

小程序开发怎么申请HTTPS证书?

小程序开发中申请SSL证书的流程包括选择信任可靠的SSL证书服务商、申请SSL证书、配置服务器、测试验证以等。以下将具体分析这个过程&#xff1a; 1、选择信任可靠的SSL证书服务商&#xff1a; 选择信任可靠的证书服务商&#xff0c;致命的证书服务商可以提供可靠的产品和完善…

【已解决】页面操作系统功能,诡异报错500nginx错误

【已解决】页面操作系统功能&#xff0c;诡异报错500nginx错误&#xff0c;后台没有任何报错信息 不知道啥原因 清理了浏览器缓存 也没有效果 还有一个表现情况&#xff0c;同样的操作&#xff0c;有时可以又是不行 因为报错ng的代理问题&#xff0c;检查了ng配置 后续经过同…

Unity强化工程 之 SpriteEditer SingleMode

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正 因为unity不只是3d需要&#xff0c;还有2d游戏需要大量编辑处理图片素材&#xff0c;所以需要了解Sprite&#xff08;精灵…

Cesium初探-实体

在 Cesium 中&#xff0c;"实体"&#xff08;Entity&#xff09;是一个核心概念&#xff0c;它代表了可以在场景中渲染的任何东西&#xff0c;从简单的点、线、多边形到复杂的模型和图像。实体可以用来表示地理空间数据&#xff0c;如地标、轨迹、卫星轨道等。 以下…

【数据结构】栈的概念、结构和实现详解

本文来介绍一下数据结构中的栈&#xff0c;以及如何用C语言去实现。 1. 栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;它只允许在固定的一端进行插入和删除元素的操作。 进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。 栈中元素遵循后进先出…

PXE实现linux系统批量自动安装

实验环境&#xff1a; 1、RHEL7主机 2、主机图形化 3、配置网络可用 4、关闭vmware dhcp功能 一、实验环境准备 1、主机图形化 在安装安装RHEL7系统时&#xff0c;选择图形化安装&#xff0c;如果没有选择&#xff0c;可以在后面通过命令安装&#xff0c;如下&#xff1…

Pinia状态管理库

为了跨组件传递JWT令牌&#xff0c;我们就会利用Pinia状态管理库&#xff0c;它允许跨组件或页面共享状态。 使用Pinia步骤&#xff1a; 安装pinia&#xff1a;cnpm install pinia 在vue应用实例中使用pinia 在src/stores/token.js中定义store 在组件中使用store 1.在main.js文…

sql注入靶场sqli-labs常见sql注入漏洞详解

目录 sqli-labs-less1 1.less1普通解法 1.在url里面填写不同的值&#xff0c;返回的内容也不同&#xff0c;证明&#xff0c;数值是进入数据库进行比对了的&#xff08;可以被注入&#xff09; 2.判断最终执行的sql语句的后面还有内容吗&#xff0c;并且能够判断是字符型的拼接…

MySQL增删改查(基础)

1、. 新增&#xff08;Create&#xff09; 语法&#xff1a; INSERT [INTO] table_name[(column [, column] ...)] VALUES (value_list) [, (value_list)] ... 例子&#xff1a; -- 创建一张学生表 DROP TABLE IF EXISTS student; CREATE TABLE student (id INT,sn INT com…

电子琴——Arduino

音调有7个音调&#xff0c;分别是哆来咪发索莱西&#xff1b;如果用蜂鸣器来发出这七个音调就要分别设置这七个音调对应频率。 电子琴实现需要物品有&#xff0c;arduino开发板一个&#xff0c;按键7个&#xff0c;蜂鸣器1个&#xff0c;杜邦线若干 重点讲一下按键原理 按键开…

linux运维一天一个shell命令之vim详解

前言&#xff1a; 在日常运维工作中&#xff0c;掌握好 Vim 的使用可以极大地提高工作的效率。Vim 作为一个强大的文本编辑器&#xff0c;广泛应用于各种运维场景 一、定义 Vim 是一个非常强大的文本编辑器&#xff0c;在 Unix/Linux 环境中非常流行。它具有许多高级功能和快…

【神软大数据治理平台-高级动态SQL(接口开发)】

1、背景 业务部门需大数据平台按照所提需求提供企业数据接口&#xff0c;基于神软大数据治理平台-高级动态SQL功能&#xff0c;满足业务需求&#xff0c;如下&#xff1a; &#xff08;1&#xff09;业务系统需求&#xff1a; 输入&#xff1a; enterpriseName&#xff1a;…

【抖音卡片】在抖音私信的时候给对方发送抖音卡片链接

效果展示 效果说明 在默认情况下&#xff0c;给对方发送连接的时候是以文本的形式展示的可点击的超链接&#xff0c;但是经过处理之后可以将你发送的连接一样卡片的形式展示。 实现步骤 第一步&#xff1a;打开微信云托管 微信云托管 (qq.com)https://cloud.weixin.qq.com/c…

原装二手MSO5204B泰克DPO5204B混合信号示波器

泰克 MSO5204B混合信号示波器&#xff0c;2 GHz&#xff0c;4 16 通道&#xff0c;10 GS/s Tektronix MSO5204B 具有出色的信号保真度和高级分析和数学功能。 当今数据速率更快、时间裕度更严格&#xff0c;因此设计时需要具有出色信号采集性能和分析功能的示波器。Tektronix…

电源芯片负载调整率测试方法、原理以及自动化测试的优势-纳米软件

在芯片设计研发领域&#xff0c;负载调整率作为稳压电源芯片的关键性能指标&#xff0c;直接关系到芯片的稳定性和可靠性&#xff0c;因此其测试和优化显得尤为重要。以下是对负载调整率测试原理、方法以及使用ATECLOUD-IC芯片测试系统优势的进一步阐述&#xff1a; 负载调整率…