记录一下多线程学习中遇到的很基础的关于参数传递的小坑,比较基础,做个笔记,有误望指出
1结论现行
多线程基础传参,务必注意以下几点:
- 传递空间已销毁
- 多线程共享访问同一块空间
- 传递的指针变量但是声明周期小于线程
- 注意平台不一样输出可能不一样
2较为基础的传参
#include <iostream>
#include <thread>
using namespace std;
class Para
{
public:
Para() { cout << "Create Para" << endl; }
Para(const Para& p) { cout << "Copy Para" << endl; }
~Para() { cout << "~Drop Para" << endl; }
string name;
};
void ThreadMain(int p1, float p2, string str, Para p4)
{
this_thread::sleep_for(1000ms);
cout << "ThreadMain " << p1 << " " << p2 << " " << str <<" "<<p4.name<< endl;
}
int main(int argc, char* argv[])
{
thread th;
float f1 = 12.1f;
Para p;
p.name = "test Para class";
//注意这里所有的参数做复制
th = thread(ThreadMain, 101, f1, "test string para",p);
th.join();
return 0;
}
输出结果:
Create Para
Copy Para
Copy Para
ThreadMain 101 12.1 test string para
~Drop Para
~Drop Para
~Drop Para
以上代码是较为基础的一个传参,要注意:所有参数将“复制“到子线程。这里表面上传递了一次对象p,但是直到结束还再调用了2次复制构造函数。
参考 https://blog.csdn.net/mingwu96/article/details/123029938中:
实参从主线程传递到子线程的线程函数中,需要经过两次传递。第1次发生在std::thread构造时,实参按值传递并以副本形式被保存到thread的tuple中,这一过程发生在主线程。第2次发生在向线程函数传递时,此次传递是由子线程发起即发生在子线程中,并将之前std::thread内部保存的副本以右值的形式(通过调用std::move())传入线程函数。
题外话:上边的程序用的是join()主线程等待子线程结束才继续运行,此时如果改成用detach()分离会如何呢?父亲买了个detach()的橘子,从此子与父分离了。以后儿子就独立了归社会管了,父亲继续跑接下来的程序。所以在debug时主线程跑完,程序就退了,看不到子线程的输出:
Create Para
Copy Para
~Drop Para
更换成detach之后,再在return前加个getchar阻塞一下不让主线程退出,最终输出结果与第一个是一样的。
3多加一个block更清晰
其他不变,主函数变成:
int main(int argc, char* argv[])
{
thread th;
{
float f1 = 12.1f;
Para p;
p.name = "test Para class";
//所有的参数做复制
th = thread(ThreadMain, 101, f1, "test string para",p);
}
th.join();
return 0;
}
输出结果:
Create Para
Copy Para
~Drop Para
Copy Para
ThreadMain 101 12.1 test string para
~Drop Para
~Drop Para
该block中创建的对象p在出了block之后析构了,但是在block中时已经复制了一份给子线程,子线程是可以正常输出的。那问题来了:这两次复制构造是怎么肥四?
4传引用会如何
类定义不变,修改入口函数和main函数,同时输出一下对象p的地址看是不是同一个对象
void ThreadMainRef(Para& p)
{
this_thread::sleep_for(100ms);
cout <<"入口函数中对象地址:" << &p << endl;
cout << "ThreadMainPtr name = " << p.name << endl;
p.name = "22222";
cout << "change name = 22222" <<endl;
}
int main(int argc, char* argv[])
{
//传递引用
Para p;
p.name = "test ref";
cout<<"主线程对象地址:" << &p <<endl;
thread th(ThreadMainRef, ref(p));
th.join();
cout << "p name in main = " << p.name<<endl;
return 0;
}
输出结果:
Create Para
主线程对象地址:0x7ffcb60cf450
入口函数中对象地址:0x7ffcb60cf450
ThreadMainPtr name = test ref
change name = 22222
p name in main = 22222
~Drop Para
这里是join等待子线程结束,可以看出对象地址是一样的,也是一次构造一次析构,所以是同一个对象。注意,去除ref()将不能通过编译,可以了解一下绑定和std::ref用法。但是,小心这里有坑,仅看他是同一个地址的话不能确保子线程看到的他还是不是原本的他,继续看。
5传指针会如何
其他不变,这里我们输出对象p的地址。同时加上block让主线程中对象p超出block析构掉,detach分离的同时getchar()阻塞一下,等待子线程的输出看是什么情况
void ThreadMainPtr(Para* p)
{
this_thread::sleep_for(100ms);
cout<<"主线程对象地址:" << p <<endl;
cout << "ThreadMainPtr name = " << p->name << endl;
}
int main(int argc, char* argv[])
{
{
//传递线程指针
Para p;
p.name = "test ThreadMainPtr name";
cout<<"主线程对象地址:" << &p <<endl;
thread th(ThreadMainPtr, &p); //错误 ,线程访问的p空间会提前释放
th.detach();
}
// Para 释放
getchar();
return 0;
}
输出结果:
Create Para
主线程对象地址:0x7ffd3a5ac320
~Drop Para
主线程对象地址:0x7ffd3a5ac320
ThreadMainPtr name = Ѝtr name
可以看出,子线程访问p空间时,p已经析构,内存中的内容更改掉了,输出乱码。实际上,不管是传引用还是传指针,如果detach分离后对象析构后子线程访问都出现这种情况。另外,测试的时候如果name的长度较小,可以正常输出,但是真实的它已经没了,留下的只是它的残影,还是找更靠谱的方法吧。
题外话:此时如果将detach改成join会怎样?会正常输出,因为它还是它,还没有析构。
6坑来了
void ThreadMainPtr(Para* p)
{
this_thread::sleep_for(100ms);
cout << "ThreadMainPtr name = " << p->name << endl;
}
int main(int argc, char* argv[])
{
{
//传递线程指针
Para p;
p.name = "test ThreadMainPtr name";
cout <<"p address:" << &p << endl;
thread th(ThreadMainPtr, &p); //错误 ,线程访问的p空间会提前释放
th.detach();
}
// Para 释放
{
//传递线程指针,这里没出错
Para p1;
p1.name = "test ThreadMainPtr name1";
cout <<"p1 address:" << &p1 << endl;
thread th1(ThreadMainPtr, &p1);
th1.join();
}
return 0;
}
输出结果:
Create Para
p address:0x7fffffffdd00
Drop Para
Create Para
p address:0x7fffffffdd00
ThreadMainPtr name = ThreadMainPtr name = test ThreadMainPtr name1test ThreadMainPtr name1
或者:
Create Para
p1 address:0x7fffffffdd00
Drop Para
Create Para
p1 address:0x7fffffffdd00
ThreadMainPtr name = test ThreadMainPtr name1
ThreadMainPtr name = test ThreadMainPtr name1
那问题来了:为什么会输出2次name1?
可以看出,第一个block结束时,p被析构了,子线程分离后短时间等待并输出指针所指的内存内容。此时,第二个block构造了p1放在刚刚被析构的p的内存位置,并传地址给领一个子线程th1,th1同样等待后输出。时间上就是那么凑巧,此时th和th1同时输出了那块内存,都输出name1。那结论就是,输出结果与线程访问该部分内存的时机很关键,明白这点很重要。
继续,让第一个子线程在新的对象p1构造在该指针指向的内存中前访问,那结果将和上一小点所说的访问了它的残影一样。比如,我们可以注释掉入口函数的sleep让其立马访问内存,并且在两个block中加getchar确保第一个子线程先访问被析构的对象的内存,
void ThreadMainPtr(Para* p)
{
// this_thread::sleep_for(100ms);
cout << "ThreadMainPtr name = " << p->name << endl;
}
int main(int argc, char* argv[])
{
{
//传递线程指针
Para p;
p.name = "test ThreadMainPtr name";
cout <<"p address:" << &p << endl;
thread th(ThreadMainPtr, &p); //错误 ,线程访问的p空间会提前释放
th.detach();
}
// Para 释放
getchar();
{
//传递线程指针,这里没出错
Para p1;
p1.name = "test ThreadMainPtr name1";
cout <<"p1 address:" << &p1 << endl;
thread th1(ThreadMainPtr, &p1);
th1.join();
}
return 0;
}
输出结果:
Create Para
p address:0x7fffffffdd00
Drop Para
ThreadMainPtr name = nUUU��f�@tr name
1
Create Para
p1 address:0x7fffffffdd00
ThreadMainPtr name = test ThreadMainPtr name1
Drop Para
继续,这里还有个坑,平台不一样输出可能不一样,实测在一个在线平台测坑来了第一个程序(输出地址不用管),输出结果分别是name和name1
Create Para
主线程对象地址:0x7ffe7fb50320
~Drop Para
Create Para
主线程对象地址:0x7ffe7fb502e0
ptr主线程对象地址:0x7ffe7fb50320
ThreadMainPtr name = test ThreadMainPtr name
ptr主线程对象地址:0x7ffe7fb502e0
ThreadMainPtr name = test ThreadMainPtr name1
~Drop Para
继续,还有一个坑是,局部变量创建在栈中这个都明白,但是输出变量的地址可能会让人怀疑栈是不是向下生长的或者奇怪编译器如何分配内存。鉴于此,希望如果在遇到如上多个block,且block中有创建更多个变量,输出地址有点不符合栈向下生长的情况时,考虑一下是否是平台区别且验证一下。
不同平台运行下边代码:
int main(int argc, char* argv[])
{
int k1;
int k2;
int k3;
cout << &k1 <<endl;
cout << &k2 <<endl;
cout << &k3 <<endl;
return 0;
}
linux+vscode输出结果,地址越来越大:
0x7fffffffdd2c
0x7fffffffdd30
0x7fffffffdd34
另一个在线平台输出结果,地址越来越小:
0x7ffe314c9e5c
0x7ffe314c9e58
0x7ffe314c9e54
7最后
回归主题,这里给出了创建线程的基础方法,且对传参方式做了对比,并记录了指针传递参数后分离线程造成的输出错误。总结,一定要关注变量及线程的生命周期,以及访问变量的时机,确保临时变量在线程生命周期中都能被正确访问。
加油~