多线程 - 线程参数与数据
大约 6 分钟
多线程 - 线程参数与数据
线程传参
临时对象(值传递)
引用传递(错误写法)
#include <iostream>
#include <thread> // 线程头文件
using namespace std;
void myprint(const int &i, char *pmybuf) // 带参数的线程的初始函数
{
cout << i << endl; // 输出:1 【调试】&i = 0x0072d9e
cout << pmybuf << endl; // 输出:this is a test! 【调试】&pmybuff = 0x004ffb64
}
int main()
{
int mvar = 1; // 【调试】&mvar = 0x0022fa58 {1}
int &mvary = mvar; // 【调试】&mvary = 0x0022fa58 {1}
char mybuf[] = "this is a test!"; // 【调试】&mybuff = 0x004ffb64
thread mythread(myprint, mvar, mybuf); // 注意这里传参会有点不一样
mythread.detach();
cout << "主线程" << endl;
return 0;
}
存在问题
- i,看似引用传参,但实际上是值传递,指向的不是同一地址!
- pmybuf,是真正的引用传参
- 不推荐用引用、detach子线程中绝对不可以用指针
解决方案
- 简单类型传值,传字符串时参数为
const string &pmybuf
,打印pmybuf.c_str()
- 这里要加
const
的原因:略
- 简单类型传值,传字符串时参数为
隐式转换传递(错误写法)
#include <iostream>
#include <thread> // 线程头文件
using namespace std;
void myprint(const int i, const string &pmybuf) // 带参数的线程的初始函数
{
cout << i << endl; // 输出:1 【调试】&i = 0x0072d9e
cout << pmybuf.c_str() << endl; // 输出:this is a test! 【调试】&pmybuff = 不同
}
int main(){
int mvar = 1; // 【调试】&mvar = 0x0022fa58 {1}
int &mvary = mvar; // 【调试】&mvary = 0x0022fa58 {1}
char mybuf[] = "this is a test!"; // 【调试】&mybuff = 0x004ffb64
thread mythread(myprint, mvar, mybuf); // 注意这里传参会有点不一样
mythread.detach();
cout << "主线程" << endl;
return 0;
}
存在问题
- mybuf什么时候转换成string?有可能主线程结束后,mybuf被回收后才转换成string,此时会出问题
解决方案
- 直接转换,不要隐式转换。因为隐式转换的过程会在子线程中
显示类型转换为临时变量(正确写法)
#include <iostream>
#include <thread> // 线程头文件
using namespace std;
void myprint(const int i, const string &pmybuf)
{
cout << i << endl;
cout << pmybuf.c_str() << endl; // 【调试】00C0D298
}
int main(){
int mvar = 1;
int &mvary = mvar;
char mybuf[] = "this is a test!";
thread mythread(myprint, mvar, string(mybuf)); // 直接转换,不要隐式转换 【调试】0079F6D0
mythread.detach();
cout << "主线程" << endl;
return 0;
}
- 隐式转换和显式转换区别(可以用线程id来调试一下并验证)
- 隐式转换
- string类的活动:一次类型转换构造函数(传参时 子线程)、一次析构函数(子线程)
- 显示转换
- string类的活动:进行一次类型转换构造函数(主线程)、一次复制构造函数(传参时 主线程)、两次析构函数(一次主线程、一次子线程)
- 区别原因
- 显示转换写法中,能确保类型转换后立刻调用复制构造函数。因为这里的复制构造函数不是传参引发的,而是Thread构造函数内部的一个行为
- 但隐式转换写法中,类型转换构造函数不是Thread完成的,和普通函数一样不能确保传参后立刻调用
- 隐式转换
- 结论
传递int这种简单类型参数,值传递
不建议使用detach
类对象(ref函数传递)
拷贝构造写法(地址不同)
#include <iostream>#include <thread> // 线程头文件
using namespace std;void myprint(const int i, const string &pmybuf){
cout << "《子线程》" << endl
<<"《线程id》"<<std::this_thread:get_id()<<endl;
}
class A // 要传递的类对象
{
public:
mutable int m_i;
A(int a):m_i(a){
cout<<"【构造函数】"<<endl
<<"【地址】"<<this<<endl
<<"【线程id】"<<std::this_thread:get_id()<<endl;
}
A(const A &a):m_i{
cout<<"【复制构造函数】"<<endl
<<"【地址】"<<this<<endl
<<"【线程id】"<<std::this_thread:get_id()<<endl;
}
~A(){
cout<<"【析构函数】"<<endl
<<"【地址】"<<this<<endl
<<"【线程id】"<<std::this_thread:get_id()<<endl;
}
}
int main(){
A aObj(10); // 类对象
thread mythread(myprint, aObj); // 直接传
mythread.join();
cout << "《主线程》" << endl
<<"《线程id》"<<std::this_thread:get_id()<<endl;
return 0;
}
- 调试结果
- 类对象一次构造函数(主线程)、一次拷贝构造函数(子线程)、两次析构函数(主线程、子线程各一次),两次构造的对象地址不同
- 这种写法可以用detach
ref写法(地址相同)
int main(){
A aObj(10); // 类对象
thread mythread(myprint, std::ref(aObj)); // 【修改】ref,真正的引用传递,令得主线程和子线程公用同一个地址的同一个对象
mythread.join();
cout << "《主线程》" << endl
<<"《线程id》"<<std::this_thread:get_id()<<endl;
return 0;
}
调试结果
- 主线程和子线程公用同一个地址的同一个对象
- 类对象一次构造函数(主线程)、一次析构情况(主线程)
- 这种写法不能用detach,会出错
深层原理
传递类对象时,避免隐式类型转换。创建线程时构建临时对象,并用引用来接
线程初始函数用引用来接,这里的引用并没有失效。如果用值传递的话,会有三次构造函数!一次默认构造、两次复制构造
这里其中的一次复制构造函数不是传参造成的,而是新建线程时的默认行为:thread构造函数内部会有一个构造tuple
选用
- 类对象全部使用ref()就挺好的
智能指针(move函数传递)
错误写法
int main(){
unique_ptr<int> ap(new int(100)); // 智能指针
thread mythread(myprint, ap); // 直接传,会报错。报错原因:unique不支持拷贝构造函数
mythread.join();
cout << "《主线程》" << endl
<<"《线程id》"<<std::this_thread:get_id()<<endl;
return 0;
}
- 报错原因
- unique不支持拷贝构造函数
移动语义方案
int main(){
unique_ptr<int> ap(new int(100)); // 智能指针
thread mythread(myprint, std::move(ap)); // 【修改】移动语义方案(本质是所有权转移)
mythread.join();
cout << "《主线程》" << endl
<<"《线程id》"<<std::this_thread:get_id()<<endl;
return 0;
}
- 调试结果
- 主线程和子线程的智能指针指向同一块地址
- 这种写法不能用detach,会出错
- 危险性
- 线程都有独立堆栈空间,主线程的智能指针指向主线程堆栈空间,在std::move后主线程智能指针为空
- 但通过new分配的内存依然在主线程中,子线程指针指向的内存在主线程中
- 当主线程结束后,内存回收,子线程智能指针指向的是主线程地址,会导致程序错乱
- 总结:也就是说当主线程和子线程存在共享内存数据时,绝对不能用detach()
共享参数
只读数据
直接使用即可,加上const更安全
#include <iostream>
#include <thread> // 线程头文件
using namespace std;
vector <int> g_v = {1,2,3}; // 【共享数据】不传参也可被线程直接使用
void myprint(const int i){
cout << "《子线程》" << endl
<<"《线程id》"<<std::this_thread:get_id()<<endl;
cout << g_v[0] << endl; // 【共享数据】直接使用
}
int main(){
vector <thread> mythreads;
for(int i=0; i<10; i++)
{
mythreads.push_back(thread(myprint, i));
}
for(auto iter=mythreads.begin(); iter!=mythreads.end(); iter++)
{
iter->join();
}
cout << "《主线程》" << endl
<<"《线程id》"<<std::this_thread:get_id()<<endl;
return 0;
}
可读可写(互斥量加锁)
这种情况下,代码写得有问题会很容易导致程序崩溃
- 关键处理:读的时候不能写、写的时候不能读/写
- 深层原因:写的操作不原子性,可能正在写的时候任务切换,发生问题
- 解决方案:用互斥量加线程锁,将共享数据的操作锁住
应用举例
- 比如火车有10个售票窗口(或者电影院网上订座),当订座时不能冲突