C++多线程传参怎么实现
这篇文章主要讲解了“C++多线程传参怎么实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C++多线程传参怎么实现”吧!
1.线程传参的过程
下面是thread的源代码
template< class Function, class... Args > explicit thread( Function&& f, Args&&... args );
源代码很复杂,反正我是看不懂。但是有一点可以确定,默认情况下实参都是按值传入产生一个副本到thread中(很多人可能都见过这句话,但可能不清楚具体细节,下面举例说明)
实参从主线程传递到子线程的线程函数中,需要经过两次传递。第1次发生在std::thread构造时,实参按值传递并以副本形式被保存到thread的tuple中,这一过程发生在主线程。第2次发生在向线程函数传递时,此次传递是由子线程发起即发生在子线程中,并将之前std::thread内部保存的副本以右值的形式(通过调用std::move())传入线程函数。
1.1 内置类型的实参
1.1.1参数按值传递
默认情况下,所有参数(含第1个参数可调用对象)均按值并以副本的形式保存在std::thread对象中的tuple里。
这一点的实现类似于std::bind(不了解bind的可以去学习一下)
void func(int& a) //左值引用 { a = 6; } int main() { int b = 1; thread t1(func,b); //错误。对实参b按值拷贝产生一个副本,将该副本存放在thread的tuple, //随后对副本 调用std::move,产生一个右值,而func中的参数a是左值 //引用,不能绑定到右值 cout << b << endl; t1.join(); return 0; }
1.1.2如果想按引用传递,则需要调用std::ref
void func(int& a) //左值引用 { a = 6; } int main() { int b = 1; thread t1(func,std::ref(b); //std::ref传参时,先会创建一个std::ref类型的临时对象, //其中保存着对b的引用。然后这个std::ref再以副本的形式保存在 //thread的tuple中。随后这个副本被move到线程函数,由于std::ref重载了 //operator T&(),因此会隐式转换为int&类型,因此起到的效果就好象b直接 //被按引用传递到线程函数中来 cout << b << endl;//b的输出为6 t1.join(); return 0; }
1.2 类类型的实参
1.2.1 传递的是左值对象
class A { private: int m_i; public: A(int i) :m_i(i) { cout << "转换构造" <<std::this_thread::get_id()<<endl; } A(const A& a):m_i(a.m_i) {cout << "拷贝构造" <<std::this_thread::get_id()<< endl;} A(A&& a):m_i(a.m_i) { cout << "移动构造" << std::this_thread::get_id()<<endl;} ~A() {cout << "析构函数" <<std::this_thread::get_id()<< endl;} }; void myPrint2(const A& a) {cout << "子线程参数地址是" <<&a<<std::this_thread::get_id()<< endl;}//4.子线程参数地址是0157D48049564 int main() { int i = 5; A myobj(i);//1.转换构造25964 6.析构函数25964 cout << "主线程id是" <<std::this_thread::get_id()<< endl;//2.主线程id是25964 thread mytobj(myPrint2,myobj); //3.拷贝构造25964 5.析构函数49564 //分析一下为什么上面会调用拷贝构造 //myobj是一个左值对象,因此调用拷贝构造来生 //成一个副本放入tuple中。这个过程发生在主线程中 mytobj.join(); return 0; }
1.2.2 传递的是临时对象(即右值对象)
class A { ...//定义与前面一样 }; void myPrint2(const A& a) //定义与前面一样 {...} //4.子线程参数地址是00DED638 30492 int main() { int i = 5; cout << "主线程id是" <<std::this_thread::get_id()<< endl;//1.主线程id是33312 thread mytobj(myPrint2,A(i));//2.转换构造33312,3.移动构造33312 //4.析构函数33312 5.析构函数30492 //首先,A(i)会调用转换构造生成一个临时对象 //随后对这个临时对象按值拷贝到thread中 // 由于临时对象是个右值,因此调用的是移动构造 //这两个构造都发生在主线程中 mytobj.join(); return 0; }
关于临时对象还有种可能
class A { ...//定义与前面一样 }; void myPrint2(const A& a) //定义与前面一样 {...} //4.子线程参数地址是00E7D800 28216 int main() { int i = 5; A a(i); //1.转换构造41312 6.析构函数41312 cout << "主线程id是" <<std::this_thread::get_id()<< endl;//2.主线程id是41312 thread mytobj(myPrint2,std::move(a));//3.移动构造41312 5.析构函数28216 //4.析构函数33312 5.析构函数30492 //因为move(a)返回的是一个右值,会调用移动构造生成到thread的 //tuple中。同样的,这一步发生在主线程中 mytobj.join(); return 0; }
1.2.3 传递的参数需要隐式类型转换
class A { ...//定义与前面一样 }; void myPrint2(const A& a) //定义与前面一样 {...} //3.子线程参数地址是00FFF7E4 28552 int main() { int i = 5; cout << "主线程id是" <<std::this_thread::get_id()<< endl;//1.主线程id是50076 thread mytobj(myPrint2,i);//2.转换构造28552 4.析构函数28552 //分析:首先i按值传入副本到thread,其类型仍然是int,这一步发生在主线程 //随后,子线程调用move向线程函数传参时,发生int到A的隐式类型转换(调用 /转换构造),这一步发生在子线程中 mytobj.join(); return 0; }
需要说明的是,我看很多人认为如果调用detach的话,一旦主线程在子线程前面结束,那么i会被销毁,导致隐式类型转换时出错。我觉得这是错误的,因为在主线程中,已经生成了一个i的副本到thread的tuple中,就算主线程结束,i被销毁,但i的副本不会,除非是像前面提到的const char*类型的指针,因为指针和指针的副本都指向同一个内存块,一旦指针指向的主线程内存被销毁,那么指针副本指向的就是被销毁的内存,导致野指针,
1.2.4 传递的参数是指针
void func(const string& s) { cout <<"子线程id是 " << std::this_thread::get_id() << endl; } int main(){ const char* name = "Santa Claus"; thread t(func, &w, name); //ok。首先name在主线程中以const char*类型作为副本被保存 //在thread中,当向线程函数func传参时,会先将之前的name副本隐式转 //换为string临时对象再调用move传给func的参数s //同时要注意,这个隐式转换发生在子线程调用时,即在子线程中创建这个临 // 时对象。这就需要确保主线程的生命周期长于子线程,否则name副本就会 /变成野指针,从而无法正确构造出string对象。 //std::thread t6(&Widget::func, &w, string(name)); //为避免上述的隐式转换可以带来的bug。可 //以在主线程先构造好一个string临时对象, //再传入thread中。这样哪怕调用的是 //detach,子线程也很安全 t.join(); //如果这里改成t.detach,并且如果主线程生命期在这行结束时(意味着主线程在子线程前面 //完成运行),就可能发生野指针现象。 }
1.3 传入智能指针unique_ptr
智能指针其实也是个模板类,这里单独拿出来讲一下
void myPrint3(unique_ptr<A> pgn) {cout << myp.get() << endl;}//00E6BEB8 int main() { unique_ptr<int> myp(new int(100)); thread mytobj(myPrint3,myp); //错误,首先unique_prt无法进行拷贝,只能移动。而myp是一个 //左值,不能对它进行移动构造产生一个副本放入thread thread mytobj(myPrint3,std::move(myp));//ok,std::move(myp)返回一个右值,因此调用移动构造产 //生一个副本放到thread中,这些都发生在主线程 mytobj.join(); return 0; }
再者,讨论一下上述代码在使用detach时的情况。在此之前看下面代码
class B { private: int m_b; public: B(int b) :m_b(b) { cout << "转换构造" << endl; } ~B() { cout << "析构函数" << endl; } }; void myPrint3(unique_ptr<B> pgn) { cout << pgn.get() << endl; } int main() { unique_ptr<B> t1(new B(5)); { unique_ptr<B> t2 = std::move(t1); cout << "时间点1" << endl; } cout << "时间点2" << endl; return 0; }
输出结果:
转换构造
时间点1
析构函数
时间点2
这说明t1被销毁时不会调用类B的析构函数,也不会释放分配的堆区内存。因为t1所含的指针由于后面的move操作已经被置空了。t2退出作用域时自动销毁,调用类的析构函数,并释放堆区内存
回过头
void myPrint3(unique_ptr<int> pgn) {cout << myp.get() << endl;} int main(){ unique_ptr<int> myp(new int(100)); thread mytobj(myPrint3,std::move(myp)); mytobj.detach();//即使主线程比子线程先结束,那么myp在销毁时也不会释放堆区内存 //此时pgn包含的指针指向那块堆区内存。 //那么pgn在C++运行时库中销毁时,会释放堆区内存,不会造成内存泄漏 //因此用detach也是安全的 }
感谢各位的阅读,以上就是“C++多线程传参怎么实现”的内容了,经过本文的学习后,相信大家对C++多线程传参怎么实现这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是蜗牛博客,小编将为大家推送更多相关知识点的文章,欢迎关注!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:niceseo99@gmail.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
评论