您好,登錄后才能下訂單哦!
這篇文章主要講解了“C++多線程傳參怎么實現”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“C++多線程傳參怎么實現”吧!
下面是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個參數可調用對象)均按值并以副本的形式保存在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 傳遞的是左值對象
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,并且如果主線程生命期在這行結束時(意味著主線程在子線程前面 //完成運行),就可能發生野指針現象。 }
智能指針其實也是個模板類,這里單獨拿出來講一下
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++多線程傳參怎么實現這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。