简单记录下自己学习和使用c++ thread过程中探的坑和知识点。有错误的地方欢迎大佬指正。
cppreference.com中原文如下:
std::terminate() is called by the C++ runtime if a joinable std::thread is destroyed or assigned to.
std::terminate()会直接使程序终止。所以在joinable()为true的thread变量离开作用域之前,可以通过调用join()或detach()来使joinable()为false,也可以通过移动来转移所有权。
在以下四种情况下,thread变量的joinable()为false:
joinable()的判断标准是是否有线程与当前thread变量关联。注意:哪怕线程已经执行完,但是没有调用join()或detach(),依然joinable()为true。
thread的移动和复制构造函数声明如下:
thread( thread&& other );
thread( const thread& ) = delete;
无法通过复制构造的方式创建新的线程或者尝试用两个thread变量表示同一个线程,只能进行所有权转移。
在新线程调用detach()的情况下,创建线程的函数可能会在线程执行完之前退出,局部变量所在的占空间被释放,并可能被下一个函数使用,此时程序的行为是不确定的。
只有在明确要新线程修改局部变量,并且通过join()等手段确保线程会在函数完成前结束。
补充:新线程访问局部变量指针很好理解,只需要把指针传入构造函数的参数中即可。访问局部变量的引用就能通过传参了,这种情况可以是传入可调用的对象,而对象的成员中存在对局部变量的引用。
// 参考effective modern c++
struct func{
int& i;
func(int& i_): i(i_) {}
void operator()() {
// do something to i
}
}
void f() {
int a = 0;
func my_func(a);
thread t(my_func);
t.detach();
}
在传递参数创建thread变量时,会首先将函数(或可调用对象)和参数通过复制或移动的方式(取决于传入的是左值还是右值)创建对应的副本,这个过程是在本地线程完成。之后对函数(或可调用对象)副本和参数副本以右值引用的参数形式在堆中创建副本的副本。最后在新线程中以最终函数(或可调用对象)的副本调用最终参数的副本。
验证代码:
struct T{
T(){};
T(const T& t) {
cout<<"copy construct: "<<&t<<" "<<this<<endl;
cout<<this_thread::get_id()<<endl;
}
T(T&& t) {
cout<<"move construct: "<<&t<<" "<<this<<endl;
cout<<this_thread::get_id()<<endl;
}
~T() {
cout<<"destruct: "<<this<<endl;
}
};
void f(T a) {
while(1);
}
int main ()
{
{
T a;
cout<<&a<<endl;
thread t(f, a);
t.detach();
}
while(1);
}
本地测试结果:
0x7ffd1a6db4ef
copy construct: 0x7ffd1a6db4ef 0x7ffd1a6db4a8
140632872105792
move construct: 0x7ffd1a6db4a8 0x559255e322c8
140632872105792
destruct: 0x7ffd1a6db4a8
destruct: 0x7ffd1a6db4ef
move construct: 0x559255e322c8 0x7fe7a4629df7
140632872101632
可以看到构造thread过程实现了一次参数的复制构造和移动构造,第一次复制构造在当前线程空间创建参数的副本,第二次移动构造应该是在堆上创建了副本的副本,所以最后离开作用域之后只有原变量和第一副本被析构。第三次的移动构造就是在新线程函数构造形参,所以新线程函数参数是以std::move()的方式传入。
注意:对不可复制类型的左值参数要使用std::move()传入,这样第一步就会调用移动构造函数,否则无法编译通过。
讲真:其实并不明白为什么第一步要复制(或移动)一份副本。
根据官方构造函数说明。
The completion of the invocation of the constructor synchronizes-with (as defined in std::memory_order) the beginning of the invocation of the copy of f on the new thread of execution.
构造函数会在新线程刚开始调用函数的时候返回。
如前面所说,所有传入新线程函数的参数都是原始参数的副本,所以任何操作都不会对原变量产生影响。如果一定要在新线程中修改原始变量的话,则可以通过std::ref()。该函数实际返回std::reference_wrapper<T>类型变量,这是一个包装类,本质上存储了参数的引用,且该引用在复制和移动时不会改变。
我们将之前的代码略作修改,在传参时使用std::ref:
thread t(f, std::ref(a));
本地输出:
0x7ffd54516b47
destruct: 0x7ffd54516b47
copy construct: 0x7ffd54516b47 0x7f67d2fdce17
140083898210048
140083898210048
可以看到过程中并没有对a的复制和移动,最后传入新线程函数的是原始变量的引用。同理,也可以对thread构造函数的第一个参数使用std::ref(),这种情况下就不会复制(或移动)函数指针或可调用对象。
考虑下面代码:
// 代码参考自c++并发实战
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024];
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer);
t.detach();
}
在用户的角度可能以为buffer会在构造函数执行前自动转换为string类型,这样在新线程就不会访问原线程的局部变量。但是实际上构造函数会将buffer指针一路复制(或移动)到新线程的存储,最后在新线程中调用f,此时才会自动构建string。但是这个时候构造线程的函数可能已经退出,局部变量无效,会导致不可预测的行为。
考虑下面代码:
struct T {
void operator()() {
}
}
thread my_thread(T());
这种通过临时变量构造thread的方式会被c++编译器解析为函数声明,函数名my_thread,该函数返回一个thread对象,参数是一个函数指针,指向没有参数并返回T对象的函数。这种情况建议使用统一初始化。
thread my_thread{T()};
全文参考:c++并发实战,effective modern c++
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。