线程池项目

  • 线程池支持的模式PoolMode枚举类型
    • enum class是限定作用域枚举类型,枚举成员的作用域限定在枚举类型所声明的作用域中
enum class PoolMode
{
	MODE_FIXED,  // 固定数量的线程
	MODE_CACHED, // 线程数量可动态增长
};
  • ThreadPool线程池类型
    • 任务队列不可以用裸指针,我们无法确定用户传进来的任务生命周期,用户只关心任务的run()是否执行,我们还要确保run()的过程中任务对象会不会突然消失,所以用一个智能指针来管理指向任务的指针
  • ThreadPool线程池类型
    • 线程列表和任务队列的部分所属变量属性在多线程环境下会出现竞态条件,必须线程互斥,不用互斥锁,太大了,原子变量即可,CAS无锁设计。
  • ThreadPool线程池类型
    • 任务队列在生产者-消费者模型中需要保持线程安全,通过互斥锁和条件和条件变量实现线程通信。
  • ThreadPool线程池类型
    • 禁止拷贝构造和拷贝赋值
  • ThreadPool线程池类型
    • 在ThreadPool的start()中创建Thread对象,绑定线程函数,方便访问变量,然后统一启动
threads_[i]->start()
std::unordered_map<int,std::unique_ptr<Thread>> threads_
  • Thread线程类型
    • 在构造函数中接收线程函数,在start()中创建真正的程,并设置线程分离,防止出了start()函数线程对象释放带着没执行完的线程函数一起结束了
  • ThreadPool--submitTask()
    • 先获得任务队列上上的锁,notFull条件变量不能等待超过一秒,要符合lambda函数的条件,每次条件变量被notify时会检测一次是否符合条件[wait_for],添加任务到任务队列,增加任务数量,notEmpty条件变量[notify_all]
  • ThreadPool--threadFunc()多线程取任务并执行
    • 锁只锁住取出任务的过程,维护任务队列的线程安全。先获得锁,notEmpty条件变量,符合lambda函数条件,不符合就一直等。取出任务,减少任务数量。两个条件变量更灵活,如果还有任务先notify条件变量notEmpty让其他可能等待线程也可以执行,再notify条件变量notFull,最后执行任务的run(),此时已经出了锁的范围。
  • 任务队列储存的是一个任务对象类型抽象类指针,传入的任务对象是一个继承抽象类的任务实例,重写其中的run(),run()方法里面就是需要执行的任务。由此解决任务队列可以存储任何任务的要求
  • 如何接受线程池中线程执行完任务后可能是任意类型的返回值
    • 模板不可以,run()本身是虚函数,返回类型是模板,错误。编译器在编译阶段会实例化所有模板,即将T转换成对应的类型,但是在编译阶段编译器根本不知道基类指针到底指向哪一个子类,只是运行时才知道的事情。所以不可以一起用
    • c++17 Any上帝类 接收任何数据类型的类型Any类的private就是一个基类,里面只有一个虚析构函数,一个继承自基类的派生类,返回值就存储在里面,用模板接受,一个基类的指针,返回的时候Any类隐式构造,创建一个指向派生类的基类。
    • 如何取出派生类里面返回值 我们在确定基类指针确定指向派生类时强转取值
  • 这里有一个细节问题就是Any()类禁止了拷贝构造和拷贝赋值,使用移动构造和移动赋值,为什么函数返回值构造Any对象的时候不报错
    这里还是有一些问题

c++类中6中函数。由于声明一种可能影响另一种是否生成,所以用default.一般四种,下三种显示声明任意一个,移动不生成,移动自己一种,另一种不生成

无参构造,有参构造,拷贝构造【初始化】,拷贝赋值【赋值】移动构造【初始化】移动赋值【赋值】

run()应该报错,因为没有拷贝拷贝构造,但是RVO优化,此时别说拷贝构造,什么也没发生。不抛出异常,不多个返回点都优化。这里不一定,编译器优化不知道发生了什么

  • 为什么Any的声明和定义都在.h文件

https://blog.csdn.net/lijiayu2015/article/details/52650790

  • c++20Semaphore底层就是条件变量和互斥锁,只是多了计数
    • 任务队列的生产者和消费者的线程通信用互斥锁和条件变量来解决,线程执行任务返回结果和用户提取结果的线程通信用信号量来解决
    • post().wait()非常简单
  • Result的设计
    • 我们通过Any上帝类解决了如何接收任务的run()函数返回任意数据类型的值的问题,此时线程处理问题的结果存储在Any上帝类中的一个指向派生类的由unique_ptr管理的基类指针上,也就是在线程池中任务队列的任务中。我们如何把这个Any传递给遥远的用户调用的submitTask.
      *注意,他们之间联系就是任务本身
      • 我们可以在task中将Any封装成Result,变成一个成员变量,用一个方法返回。在submitTask中调用这个方法得到Result,通过Result的方法的到Any对象,调用cast_<>()的到run()返回值。但是这里有一个问题,这个返回的Result对象完全依托任务队列中的task存在,可是线程在执行完任务后会让任务出队列,任务消失。我们此时在调用Result对象就会出现问题
      • 通过task,创建一个独立的Result对象才是这个项目中的正解。在task中创建一个Result指针,注意必须是裸指针,千万不要shared_ptr,不然循环引用。Result中调用task的方法设置这个裸指针,然后用一个函数封装一下run(),之后就执行这个函数,在里面多调用一个Result对象设置自己Any对象的方法,task的run()执行完,Result得到返回值,task出队列,但是RResult里面有个强智能指针指向它,不会释放。
  • 通过一个与原子布尔值isPoolRunning_【线程池的运行状态,默认false,stat()置true】来防止用户没有在线程启动之前设置运行模式。以及为之后的资源释放做准备
  • idleThreadSize记录空闲线程数量,初始0.每次线程对象start()时加一,回收线程减一,过了notEmpty条件变量也减一,执行完任务+1
  • 在submitTask()中处理cached模式下的逻辑,剩余任务数量是否大于空闲线程数量且当前线程数量小于线程阈值。创建线程对象,绑定线程函数,加入线程列表,启动线程,修改线程相关变量
  • threadFunc()下也要处理cached模式下的逻辑,线程在notEmpty条件变量上是因为超时时间返回,空闲时间大于空闲时间阈值且线程数量大于初始线程数量。回收线程,return即可,线程函数结束线程结束,从线程列表中取出,当初的vector无法取出,用unordered_map取出,key是一个静态的递增id.这个id只是方便在map中找到线程对象,真的线程id调用get_id()方法,修改相关线程变量

死锁情况分析


线程回收的代码逻辑在线程池的析构函数和线程函数threadFunc()中,线程池对象在回收之前必须等待线程资源全部回收,即线程的函数都要执行完,要么直接return,要么跳出线程函数中的最大的while(isPoolRunning)循环,最终都是线程函数结束,线程结束,更新线程列表以及exitCond_.notify_all(),线程列表长度0线程池对象才能回收
具体的说
当线程池对象释放时,在线程池的析构函数中,先置原子布尔值isPollRunning为false,这是为了正在执行任务的线程在任务执行完成后再一次面对while(isPoolRunning)循环时不在进入循环,然后这个线程更新线程列表以及exitCond_.notify_all(),这个线程结束线程函数,释放,线程池对象在条件变量上从等待到阻塞,在获得锁之后判断线程列表是否为空。还有notEmpty_.notity_all(),无论是cached还是fixed模式下等待在条件变量上的线程,从等待到阻塞,获得锁,往下走,进入if(!isPollRunning)中,和上面一样的事情,只是return结束线程函数
但是,还有一种线程情况没有考虑,这导致程序可能会出现不确定的死锁,即线程处于进入while循环但是还没有在条件变量上等待且在会进入第二层循环[没有任务,有的话不进入第二层循环就去执行任务了,那就是第二种线程情况了]的情况,注意我们这里线程情况是指析构函数开始执行时的线程情况且不能是cached模式,这个模式的线程池线程不会一直等待在notEmpty_条件变量上,会超时判断

  • 第一种可能用户线程先获得锁,在条件变量exitCond_上等待,释放锁。线程池线程获得锁,在notEmpty_条件变量上等待,释放锁,前者等待线程池线程结束时唤醒,后者等待用户线程结束时唤醒,但是用户线程在等待之前就唤醒了,死锁成立
  • 第二种可能线程池线程先获得锁,在notEmpty_条件变量上等待,释放锁,加入在这之前用户线程已经notity了条件变量notEmpty_,目前正在等待获取锁,那么此时获取锁,然后在条件变量exitCond_上等待,释放锁。死锁成立
    • 解决第一种死锁的方法锁+双重判断我们在第一循环中已经判断线程池状态,我们在后获得锁后面对第二层循环再次判断线程池状态,不进入循环,变成第二种线程情况。此时我们干脆让取消第二层循环中的if(!isPollRunning)代码,改为在第二循环外面,执行任务代码之前加一个if(!isPollRunning){break;}。使三种线程情况的线程释放代码合并死锁解决
    • 解决第二种死锁方法*让用户线程在获得锁后再notity条件变量notEmpty_死锁解决

Linux多线程死锁问题之gdb调试

  • 示例程序
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
#include <vector>

// 锁资源1
std::mutex mtx1;
// 锁资源2
std::mutex mtx2;

// 线程A的函数
void taskA()
{
    	// 保证线程A先获取锁1
    	std::lock_guard<std::mutex> lockA(mtx1);
    	std::cout << "线程A获取锁1" << std::endl;
    
    	// 线程A睡眠2s再获取锁2,保证锁2先被线程B获取,模拟死锁问题的发生
    	std::this_thread::sleep_for(std::chrono::seconds(2));
    
    	// 线程A先获取锁2
    	std::lock_guard<std::mutex> lockB(mtx2);
    	std::cout << "线程A获取锁2" << std::endl;
    
    	std::cout << "线程A释放所有锁资源,结束运行!" << std::endl;
}

// 线程B的函数
void taskB()
{
    	// 线程B先睡眠1s保证线程A先获取锁1
    	std::this_thread::sleep_for(std::chrono::seconds(1));
    	std::lock_guard<std::mutex> lockB(mtx2);
    	std::cout << "线程B获取锁2" << std::endl;
    
    	// 线程B尝试获取锁1
    	std::lock_guard<std::mutex> lockA(mtx1);
    	std::cout << "线程B获取锁1" << std::endl;
    
    	std::cout << "线程B释放所有锁资源,结束运行!" << std::endl;
}
int main()
{
	// 创建生产者和消费者线程
	std::thread t1(taskA);
	std::thread t2(taskB);

	// main主线程等待所有子线程执行完
	t1.join();
	t2.join();

	return 0;
}

  • 运行结果
  • 虚拟机太垃圾,更换腾讯云服务器
  • ps命令查看一个进程的PID和运行状态

Sl+:一个在前台运行正在等待某事件发生的多线程程序

  • top命令查看进程中线程的具体状态

三个线程全部处于阻塞状态[S],且cpu占有率都是0.0,则排除死循环问题。要么是由于没有IO网络事件使线程阻塞,比如说muduo中baseloop和subloop全部阻塞在IO多路复用上。要么是发生了死锁问题。

  • gdb远程调试正在运行的程序,打印进程中每一个线程的调用堆栈信息

Thread3即tasB线程,从下往上看,最后阻塞在wait上

Thread2即tasA线程,从下往上看,最后阻塞在wait上

main线程最后阻塞在join上

  • 在源码中直接定位问题代码
  • 重新编译与运行
  • 查看进程
  • gdb调试该进程并查看当前所有线程

  • 切换到thread 2[taskA],调用堆栈信息

    • 查看第5帧调用信息,发现问题代码

Windows死锁问题之windbg调试b站dvlinker*

gdb扩展

  • 常用命令

b break 下断点
r run 运行程序,直到遇断点停止
n next 运行本行代码,遇到函数不会进入内部
s step 遇到函数会进入内部
p print 输出变量的值
c continue继续运行程序,遇到断点停止
q quit退出gdb
set var xx=xx 设置变量的值
set args xx.cc/xx/xx 设置传入程序的参数,参数包含特殊字符要用双引号包含
list 查看源代码
info br 查看有哪些断点
clear xx删除某一行代码或某个函数的所有断点
delete xx删除某一个断点或者全部断点

将项目编译成动态库

  • 文件无法上传,chown 777 xxx 修改创建的项目文件夹权限
  • xftp显示的文件名乱码,修改属性编码为UTF-8
  • 上传文件中中文乱码,在windows上修改文件编码UTF-8
  • xshell输出有乱码,修改日志属性UTF-8
  • 是什么不重要,重要的是统一
  • 编译动态库
    • linux下和windows下库不一样,包含一下#include<thread>
  • 编译测试文件tTest.cpp
    • 动态库文件libtpool.so放到/usr/local/lib/下
    • threadpool.h文件放到/usr/local/include/xia
    • g++ tTeat.cpp -ltpool -lpthread -std=c++17
  • 运行a.out文件出现问题,找不到动态库文件
    • 编译时找动态库的地方和运行时找动态库的地方不一样
    • g++会去访问/etc/文件夹下的ld.so.conf文件,其中包括同级目录下的ld.so.conf.d文件下的所有*.conf文件,我们在其中创建一个mylib.conf文件,写入/usr/local/lib/即可。

在linux上运行时出现致命的死锁问题

  • gdb远程监控阻塞的进程,查看所有线程的情况,所有线程全部死锁。主线程死锁在析构函数的条件变量上,线程池线程全部死锁,没有人notity它,线程列表又不是0,自然在这等待着,死锁,这个可以理解。问题在于线程池线程竟然死锁在了我自己实现的一个信号量上的post函数中的一个条件变量notity上。
  • 不仅线程池会析构,result【submitTask中返回的result】对象也会析构,默认析构,成员变量析构,Semaphore变量析构,默认析构,成员变量析构,条件变量析构。在vs上条件变量的析构会释放资源,所以运行到这个条件变量notity也不会有未知的行为,资源释放了就什么都不做,线程依旧运行,反正线程此时不取result的值,也不会到运行到Semaphore对象的wait()中,不会死锁在那【取的话这个线程池线程也不会死锁,因为用户线程会阻塞在get上,不会像不取一样资源对释放了的条件变量notity】完美。但是在linux中条件变量的析构函数什么也不做,导致这个条件变量状态失效,又在这个未知状态条件变量上notitty,就无故阻塞了。此时死锁成立

解决方法

  • 治标每个submitTask返回的result都get()cast_<>一下
  • 治本Semaphore析构函数不默认,反转一个布尔值。post()函数中如果条件变脸析构了就直接return,什么都不做,间接达到和vs下一样的效果

项目重构

  • 项目值得改造的地方?
    • 旧版项目中我们必须写一个继承自虚拟基类task的任务类,在其中重写run()方法,在submitTask()方法提交任务的时候还必须用形参基类std::shared_ptr<Task>智能指针指向传进来的派生类实参td::make_shared<MyTask>(xx.xx.xx)产生的智能指针,使任务队列可以接受各种各样的任务,并按照要求填写好构造参数,使这些数据可以被run()函数使用。但是在现代c++面前这太臃肿了
    • 为了run()可以返回的各种各样返回值且由于这个run()是一个虚函数而无法使用模板,所以只能实现一个Any上帝类作为run()的返回类型。又由于我们的用户线程要得到这个储存了返回值的Any,所以我们实现了Result类,在构造时接收当前任务,然后在当前任务的虚拟基类中储存自己的裸指针,通过这个将Any对象赋值给Result对象,它可以通过自己的方法得到Any对象,再得到Any对象中存储的返回值。同时为了保证线程池线程处理任务,给Result对象赋值Any与用户线程取返回值之间的线程通信,我们又自己实现了一个信号量。很优美的设计,但是在现代c++面前还是臃肿

使用可变参模板和future机制来改造项目

  • 去除掉除了线程池类,线程类和PoolMode枚举类型以外的所有设计,包括虚拟基类Task,Any上帝类,Result结果类和Semphore信号量类,并对剩下的代码进行修改

前置知识点

1.引用折叠和完美转发

  • 什么是左值?什么是右值?
    • 有名字可以取出地址的都是左值,没有名字,取不出地址的都是右值注意右值引用的变量是一个左值,int&&a=10,有名字有地址,a就是一个左值
  • 引用折叠的由来
    • 引用折叠的概念主要用在函数模板类型参数的推导中,像T&&这种万能引用类型,利用模板推导和引用折叠实例化模板来接受传进来的参数,右值引用和右值都成右值引用,左值引用和左值都成左值引用。int && + &&折叠成int&&,除此之外,都折叠成int&
  • std::move(),返回一个左值强转得到的右值引用类型主要用在调用移动构造函数的时候
  • forward完美转发机制

重点笔记https://www.cnblogs.com/S1mpleBug/p/16703328.html

2.可变参模板

  • 全特化和偏特化
  1. 函数模板必须全特化
//模板函数
template<typename T1, typename T2>
void fun(T1 a , T2 b) {
	cout << "模板函数" << endl;
}

//全特化
template<>
void fun<int, char >(int a, char b) {
	cout << "全特化" << endl;
}

//函数不存在偏特化:下面的代码是错误的
/*
template<typename T2>
void fun<char, T2>(char a, T2 b) {
	cout << "偏特化" << endl;
}
*/

  1. 类模板全特化和偏特化都可以
// 类模板
template<typename T1, typename T2>
class Test {
public:
	Test(T1 i, T2 j) : a(i), b(j) { cout << "模板类" << endl; }
private:
	T1 a;
	T2 b;
};

// 类模板全特化
template<>
class Test<int, char> {
public:
	Test(int i, char j) : a(i), b(j) { cout << "全特化" << endl; }
private:
	int a;
	char b;
};

// 类模板偏特化
template <typename T2>
class Test<char, T2> {
public:
	Test(char i, T2 j) : a(i), b(j) { cout << "偏特化" << endl; }
private:
	char a;
	T2 b;
};


Test<double, double> t1(0.1, 0.2); // 类模板
Test<int, char> t2(1, 'A'); // 模板全特化
Test<char, bool> t3('A', true); // 模板偏特化

可变参函数模板如何展开参数包

  • 以递归的方法取出可用参数
void print() {}//当所有参数全部打印,最后输入的参数为空,会调用这个,防止报错

template<typename T, typename... Args>
void print(const T& firstArg, const Args&... args) {//接受>=1个数量的参数
	std::cout << firstArg << " " << sizeof...(args) << std::endl;//打印第一个参数和剩余参数数量
	print(args...);递归调用,此时第一个参数已经打印,所以只需要输入剩下的参数列表
}

下面这个程序功能上面一样,但是涉及到知识点当泛化和特化的模板函数同时存在的时候,程序会选择较为特化的那一个

template<typename T>//处理只有一个参数的情况
void print(T arg)
{
    std::cout << arg <<" "<<0<<std::endl;
}


template<typename T, typename... Args>
void print(const T& firstArg, const Args&... args) {//接受>=1个数量的参数
	std::cout << firstArg << " " << sizeof...(args) << std::endl;//打印第一个参数和剩余参数数量
	print(args...);递归调用,此时第一个参数已经打印,所以只需要输入剩下的参数列表
}
  • 运用实例,可变参模板找最大值
#include <iostream>

template <typename T>
T my_max(T value) {
  return value;
}

template <typename T, typename... Types>
T my_max(T value, Types... args) {
  return std::max(value, my_max(args...));
}

int main(int argc, char *argv[]) {
  std::cout << my_max(1, 5, 8, 4, 6) << std::endl;

	return 0;
}

  • 递归展开参数包的问题在于必须同名的更特化的重载函数作为递归的终止条件,我们可以通过逗号表达式来展开参数包
#include <iostream>
#include <functional>
using namespace std;

template<typename T, typename ...Args>
void expand(const T &func, Args&&... args) {
    // 最重要的就是一行
    //利用逗号表达式执行前面的函数,返回后面的0
    //利用初始化列表和可变参...,展开模板,最后arr是一个size...(args)长度的0数组,每个参数也都传进了lambda函数
    int arr[] = { (func(std::forward<Args>(args)), 0)... };
    //如果用递归展开参数包,不仅要写终止函数,而且多谢一个参数来提取每一次递归时展开的那个参数
}

int main() {
    //泛型lambda接受任意类型数据
    expand([](auto i)->void{cout << i << endl;}, 1, 2, 3);
    return 0;
}

可变参类模板如何展开参数包

  • 偏特化和递归
  • enum的作用
  • 可以用integral_constant代替enum

重点笔记https://www.cnblogs.com/S1mpleBug/p/16834298.html

3.auto与deltype类型说明符

  • const

  • 补充说明之常量引用

    • 如果引用左值,不可以通过引用改变左值的值,但是却可以通过其他途径修改,包括直接修改和用另一个引用
    • 如果引用右值,一般来说左值引用初始化必须还左值,但是常量应用却不一样,它可以引用右值
    • 实践意义
      • 常量引用来做函数的形参
      • 用一个函数的局部变量去初始化一个常量引用,那这个临时变量的生命周期就会被延长,直到引用被销毁
        • 可以用一个基类的引用指向一个子类的临时变量,然后通过这个引用来实现多态,但又不用处理子类的销毁

https://blog.csdn.net/weixin_43335392/article/details/106404619

  • 补充说明之顶层const和底层const
    • 牢记顶层const就是这个变量本身是常量,不可以改变,const int a =1;const int &a=1; int b=1,int* const a=&b。底层const则是变量指向的地址上的值不可以改变。
    • 规则总结
      • 顶层const在赋值给其他变量时,可以忽略顶层属性
      • 底层const在赋值给其他变量时,不能忽略底层属性
      • 底层const无法转换为顶层const,反过来却可以
      • int*类型可以转换为顶层和底层const,所以它可以给顶层和底层的const赋值。这句话往牢记上靠,没那么玄乎,就是普通的赋值

https://blog.csdn.net/weixin_43744293/article/details/117955427

  • auto 根据初始化表达式推导变量的类型
  1. 规则1:声明为auto(不是auto&)的变量,忽视掉初始化表达式的顶层const。即对有const的普通类型(int、double等)忽视const,对常量指针(顶层const)变为普通指针,对指向常量(底层const)的常量指针(顶层cosnt)变为指向常量的指针(底层const)。同时也会忽略引用
  2. 若希望auto推导的是顶层const,加上const,即const auto。
  3. 声明为auto&的变量,保持初始化表达式的顶层const
  • 实际应用
  1. 代替冗长复杂的变量声明
    • 通过迭代器遍历容器时声明begin()
  2. 定义模板参数时,用于声明依赖模板参数的变量
template <typename _Tx,typename _Ty>
void Multiply(_Tx x, _Ty y)
{
    auto v = x+y;
    std::cout << v;
}

https://blog.csdn.net/weixin_43744293/article/details/117440727

  • decltype返回()操作数的类型

  1. decltype + 变量(加个()啥的就是表达式了)
    • 当使用decltype(var)的形式时,decltype会直接返回变量的类型包括顶层const和引用
    • 例外不会将数组转换成初始地址为数组首位的指针,在decltype(var)->decltype(var)*
  2. decltype + 表达式
    • decltype并不会实际计算表达式的值,编译器分析表达式并得到它的类型.
      decltype(expr)的结果根据expr的结果不同而不同:expr返回左值,得到该类型的左值引用;expr返回右值,得到该类型
int i = 42, *p = &i, &r = i;

// r + 0是一个表达式
// 算术表达式返回右值
// b是一个int类型
decltype(r + 0) b;

// c是一个int &
decltype(*p) c = i;

  1. decltype + 函数
// 声明了一个函数类型
using FuncType = int(int &, int);

// 下面的函数就是上面的类型
int add_to(int &des, int ori);

// 声明了一个FuncType类型的指针
// 并使用函数add_to初始化
FuncType *pf = add_to;
//decltype(add_to) *pf = add_to;可以省略using那一步

int a = 4;

// 通过函数指针调用add_to
pf(a, 2);

https://blog.csdn.net/u014609638/article/details/106987131

  • auto与decltype的综合应用

    *泛型编程中,用于追踪函数的返回值类型
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty)
{
    return x*y;
}

4.future和packaged_task和promise和async

  1. 为什么要有std::ref()
  • 主要用于向thread对象的线程函数传引用,或者在bind绑定函数参数的时候不使用placeholder::_x占位符的情况下传引用,不然都是浅拷贝。这里注意一下这个bind绑定参数不使用占位符和使用占位符对返回的函数类型的影响
    std::ref()主要就是在这两种情况下做引用传递的
  1. future和promise主要为了解决例如在用户线程开启子线程,我们希望在用户线程中得到子线程中线程函数中的值这种情况
    • 一般来说是传入一个全局变量的引用在线程函数中赋值给他,同时还有通过锁+条件变量来维护线程通信的安全性
//在主线程中绑定future和promise的对象
std::promise<int> p1;
std::future<int> f1=p1.get_future();

std::promise<int> p2;
std::future<int> f2=p2.get_future();
//我们可以将p1传进线程函数中
p1[这个看参数啊].set_value()
//然后在主线程中得到这个值
f1.get()
//并且都是线程安全的

//也可以将f2传进线程函数中,在函数中等待主线程的值
f2.get()
//主线程赋值
p2.set_value()

//注意注意注意!!!如果你在一个线程函数中都使用了,注意顺序,别死锁了


//我们还可以往多个线程中传入同一个future,就是shared_future
std::promise<int> p;
std::future<int> f=p.get_future();
std::shared_future<int> s_f=f.shared_future()
//可以在线程函数中多次get(),传的时候也不用ref

  1. packaged_task
  • std::packaged_task它包装了一个可调用的目标(如function, lambda expression, bind expression, or another function object),以便异步调用,它和promise在某种程度上有点像,promise保存了一个共享状态的值,而packaged_task保存的是一 个函数。
std::packaged_task<int()> task([](){ return 7; });//也可以是一个bind函数
std::thread t1(std::ref(task)); 
std::future<int> f1 = task.get_future(); 
auto r1 = f1.get();
  1. 为什么有了promise后还需要packaged_task?
  • 一个是我承诺会给你一个美好的未来,一个是我会给你创造一个美好的未来
  1. 异步接口async简化到啥都不需要管,连线程都不需要自己创建了
int task(int a,int b){
    return a+b;
}
std::future<int> f = std::async(task,1,2)//任务函数,参数,参数
//此时的task是一有返回值的正常函数
std::future<int> f = std::async(std::launch::async,task,1,2)
//在调用async就开始创建线程。
std::future<int> f = std::async(std::launch::deferred,task,1,2)
//延迟加载方式创建线程。调用async时不创建线程,直到调用了future的get或者wait时才创建线程。
f.get()
//此时

future的一些其他方法https://www.cnblogs.com/haippy/p/3280643.html

5.thread的参数如果是引用为什么不能直接传,而要通过std::ref()修饰

  1. 通过vs2019报错信息我们知道问题出在invoke函数
  2. 这个函数的将实参以右值的形式传递给线程函数的形参,一个左值引用是无法接收一个右值的,可以改成常量引用和右值引用。
  3. 为什么std::ref()可以
    • 它返回一个reference_wrapper对象,这个对象调用自身的opredtor()重载函数,转换成对应的引用类型。


  • 函数对象使用中调用的方法

    特别注意初始化时的临时变量优化和static变量数据段在析构时全析构了才析构
  • 函数调用过程中对象调用的方法,不考虑编译器优化
  • 三条对象优化规则
  1. 函数的参数尽量按引用传递对象,不要按值传递对象,减少实参赋值给形参的拷贝构造和出函数时的析构
  2. 函数返回对象的时候不要返回局部变量对象,应该返回临时对象,这样在出函数时,临时对象拷贝构造同类型新对象编译器优化,省去这个临时对象的构造和析构。
  3. 接收返回值是对象的函数调用时,尽量用来初始化,不要用来赋值
posted @ 2024-06-27 11:56  bbkkp  阅读(28)  评论(0)    收藏  举报