C++多線程傳參的實(shí)現(xiàn)方法
1.線程傳參的過(guò)程
下面是thread的源代碼
template< class Function, class... Args > explicit thread( Function&& f, Args&&... args );
源代碼很復(fù)雜,反正我是看不懂。但是有一點(diǎn)可以確定,默認(rèn)情況下實(shí)參都是按值傳入產(chǎn)生一個(gè)副本到thread中(很多人可能都見(jiàn)過(guò)這句話,但可能不清楚具體細(xì)節(jié),下面舉例說(shuō)明)
實(shí)參從主線程傳遞到子線程的線程函數(shù)中,需要經(jīng)過(guò)兩次傳遞。第1次發(fā)生在std::thread構(gòu)造時(shí),實(shí)參按值傳遞并以副本形式被保存到thread的tuple中,這一過(guò)程發(fā)生在主線程。第2次發(fā)生在向線程函數(shù)傳遞時(shí),此次傳遞是由子線程發(fā)起即發(fā)生在子線程中,并將之前std::thread內(nèi)部保存的副本以右值的形式(通過(guò)調(diào)用std::move())傳入線程函數(shù)。
1.1 內(nèi)置類型的實(shí)參
1.1.1參數(shù)按值傳遞
默認(rèn)情況下,所有參數(shù)(含第1個(gè)參數(shù)可調(diào)用對(duì)象)均按值并以副本的形式保存在std::thread對(duì)象中的tuple里。
這一點(diǎn)的實(shí)現(xiàn)類似于std::bind(不了解bind的可以去學(xué)習(xí)一下)
void func(int& a) //左值引用
{
a = 6;
}
int main()
{
int b = 1;
thread t1(func,b); //錯(cuò)誤。對(duì)實(shí)參b按值拷貝產(chǎn)生一個(gè)副本,將該副本存放在thread的tuple,
//隨后對(duì)副本 調(diào)用std::move,產(chǎn)生一個(gè)右值,而func中的參數(shù)a是左值
//引用,不能綁定到右值
cout << b << endl;
t1.join();
return 0;
}1.1.2如果想按引用傳遞,則需要調(diào)用std::ref
void func(int& a) //左值引用
{
a = 6;
}
int main()
{
int b = 1;
thread t1(func,std::ref(b); //std::ref傳參時(shí),先會(huì)創(chuàng)建一個(gè)std::ref類型的臨時(shí)對(duì)象,
//其中保存著對(duì)b的引用。然后這個(gè)std::ref再以副本的形式保存在
//thread的tuple中。隨后這個(gè)副本被move到線程函數(shù),由于std::ref重載了
//operator T&(),因此會(huì)隱式轉(zhuǎn)換為int&類型,因此起到的效果就好象b直接
//被按引用傳遞到線程函數(shù)中來(lái)
cout << b << endl;//b的輸出為6
t1.join();
return 0;
}1.2 類類型的實(shí)參
1.2.1 傳遞的是左值對(duì)象
class A {
private:
int m_i;
public:
A(int i) :m_i(i) { cout << "轉(zhuǎn)換構(gòu)造" <<std::this_thread::get_id()<<endl; }
A(const A& a):m_i(a.m_i) {cout << "拷貝構(gòu)造" <<std::this_thread::get_id()<< endl;}
A(A&& a):m_i(a.m_i) { cout << "移動(dòng)構(gòu)造" << std::this_thread::get_id()<<endl;}
~A() {cout << "析構(gòu)函數(shù)" <<std::this_thread::get_id()<< endl;}
};
void myPrint2(const A& a)
{cout << "子線程參數(shù)地址是" <<&a<<std::this_thread::get_id()<< endl;}//4.子線程參數(shù)地址是0157D48049564
int main() {
int i = 5;
A myobj(i);//1.轉(zhuǎn)換構(gòu)造25964 6.析構(gòu)函數(shù)25964
cout << "主線程id是" <<std::this_thread::get_id()<< endl;//2.主線程id是25964
thread mytobj(myPrint2,myobj); //3.拷貝構(gòu)造25964 5.析構(gòu)函數(shù)49564
//分析一下為什么上面會(huì)調(diào)用拷貝構(gòu)造
//myobj是一個(gè)左值對(duì)象,因此調(diào)用拷貝構(gòu)造來(lái)生
//成一個(gè)副本放入tuple中。這個(gè)過(guò)程發(fā)生在主線程中
mytobj.join();
return 0;
}1.2.2 傳遞的是臨時(shí)對(duì)象(即右值對(duì)象)
class A {
...//定義與前面一樣
};
void myPrint2(const A& a) //定義與前面一樣
{...} //4.子線程參數(shù)地址是00DED638 30492
int main() {
int i = 5;
cout << "主線程id是" <<std::this_thread::get_id()<< endl;//1.主線程id是33312
thread mytobj(myPrint2,A(i));//2.轉(zhuǎn)換構(gòu)造33312,3.移動(dòng)構(gòu)造33312
//4.析構(gòu)函數(shù)33312 5.析構(gòu)函數(shù)30492
//首先,A(i)會(huì)調(diào)用轉(zhuǎn)換構(gòu)造生成一個(gè)臨時(shí)對(duì)象
//隨后對(duì)這個(gè)臨時(shí)對(duì)象按值拷貝到thread中
// 由于臨時(shí)對(duì)象是個(gè)右值,因此調(diào)用的是移動(dòng)構(gòu)造
//這兩個(gè)構(gòu)造都發(fā)生在主線程中
mytobj.join();
return 0;
}關(guān)于臨時(shí)對(duì)象還有種可能
class A {
...//定義與前面一樣
};
void myPrint2(const A& a) //定義與前面一樣
{...} //4.子線程參數(shù)地址是00E7D800 28216
int main() {
int i = 5;
A a(i); //1.轉(zhuǎn)換構(gòu)造41312 6.析構(gòu)函數(shù)41312
cout << "主線程id是" <<std::this_thread::get_id()<< endl;//2.主線程id是41312
thread mytobj(myPrint2,std::move(a));//3.移動(dòng)構(gòu)造41312 5.析構(gòu)函數(shù)28216
//4.析構(gòu)函數(shù)33312 5.析構(gòu)函數(shù)30492
//因?yàn)閙ove(a)返回的是一個(gè)右值,會(huì)調(diào)用移動(dòng)構(gòu)造生成到thread的
//tuple中。同樣的,這一步發(fā)生在主線程中
mytobj.join();
return 0;
}1.2.3 傳遞的參數(shù)需要隱式類型轉(zhuǎn)換
class A {
...//定義與前面一樣
};
void myPrint2(const A& a) //定義與前面一樣
{...} //3.子線程參數(shù)地址是00FFF7E4 28552
int main() {
int i = 5;
cout << "主線程id是" <<std::this_thread::get_id()<< endl;//1.主線程id是50076
thread mytobj(myPrint2,i);//2.轉(zhuǎn)換構(gòu)造28552 4.析構(gòu)函數(shù)28552
//分析:首先i按值傳入副本到thread,其類型仍然是int,這一步發(fā)生在主線程
//隨后,子線程調(diào)用move向線程函數(shù)傳參時(shí),發(fā)生int到A的隱式類型轉(zhuǎn)換(調(diào)用
/轉(zhuǎn)換構(gòu)造),這一步發(fā)生在子線程中
mytobj.join();
return 0;
}
需要說(shuō)明的是,我看很多人認(rèn)為如果調(diào)用detach的話,一旦主線程在子線程前面結(jié)束,那么i會(huì)被銷毀,導(dǎo)致隱式類型轉(zhuǎn)換時(shí)出錯(cuò)。我覺(jué)得這是錯(cuò)誤的,因?yàn)樵谥骶€程中,已經(jīng)生成了一個(gè)i的副本到thread的tuple中,就算主線程結(jié)束,i被銷毀,但i的副本不會(huì),除非是像前面提到的const char*類型的指針,因?yàn)橹羔樅椭羔樀母北径贾赶蛲粋€(gè)內(nèi)存塊,一旦指針指向的主線程內(nèi)存被銷毀,那么指針副本指向的就是被銷毀的內(nèi)存,導(dǎo)致野指針,
1.2.4 傳遞的參數(shù)是指針
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中,當(dāng)向線程函數(shù)func傳參時(shí),會(huì)先將之前的name副本隱式轉(zhuǎn)
//換為string臨時(shí)對(duì)象再調(diào)用move傳給func的參數(shù)s
//同時(shí)要注意,這個(gè)隱式轉(zhuǎn)換發(fā)生在子線程調(diào)用時(shí),即在子線程中創(chuàng)建這個(gè)臨
// 時(shí)對(duì)象。這就需要確保主線程的生命周期長(zhǎng)于子線程,否則name副本就會(huì)
/變成野指針,從而無(wú)法正確構(gòu)造出string對(duì)象。
//std::thread t6(&Widget::func, &w, string(name)); //為避免上述的隱式轉(zhuǎn)換可以帶來(lái)的bug???
//以在主線程先構(gòu)造好一個(gè)string臨時(shí)對(duì)象,
//再傳入thread中。這樣哪怕調(diào)用的是
//detach,子線程也很安全
t.join(); //如果這里改成t.detach,并且如果主線程生命期在這行結(jié)束時(shí)(意味著主線程在子線程前面
//完成運(yùn)行),就可能發(fā)生野指針現(xiàn)象。
}1.3 傳入智能指針unique_ptr
智能指針其實(shí)也是個(gè)模板類,這里單獨(dú)拿出來(lái)講一下
void myPrint3(unique_ptr<A> pgn) {cout << myp.get() << endl;}//00E6BEB8
int main() {
unique_ptr<int> myp(new int(100));
thread mytobj(myPrint3,myp); //錯(cuò)誤,首先unique_prt無(wú)法進(jìn)行拷貝,只能移動(dòng)。而myp是一個(gè)
//左值,不能對(duì)它進(jìn)行移動(dòng)構(gòu)造產(chǎn)生一個(gè)副本放入thread
thread mytobj(myPrint3,std::move(myp));//ok,std::move(myp)返回一個(gè)右值,因此調(diào)用移動(dòng)構(gòu)造產(chǎn)
//生一個(gè)副本放到thread中,這些都發(fā)生在主線程
mytobj.join();
return 0;
}
再者,討論一下上述代碼在使用detach時(shí)的情況。在此之前看下面代碼
class B {
private:
int m_b;
public:
B(int b) :m_b(b) { cout << "轉(zhuǎn)換構(gòu)造" << endl; }
~B() { cout << "析構(gòu)函數(shù)" << 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 << "時(shí)間點(diǎn)1" << endl;
}
cout << "時(shí)間點(diǎn)2" << endl;
return 0;
}
輸出結(jié)果:
轉(zhuǎn)換構(gòu)造
時(shí)間點(diǎn)1
析構(gòu)函數(shù)
時(shí)間點(diǎn)2
這說(shuō)明t1被銷毀時(shí)不會(huì)調(diào)用類B的析構(gòu)函數(shù),也不會(huì)釋放分配的堆區(qū)內(nèi)存。因?yàn)閠1所含的指針由于后面的move操作已經(jīng)被置空了。t2退出作用域時(shí)自動(dòng)銷毀,調(diào)用類的析構(gòu)函數(shù),并釋放堆區(qū)內(nèi)存
回過(guò)頭
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();//即使主線程比子線程先結(jié)束,那么myp在銷毀時(shí)也不會(huì)釋放堆區(qū)內(nèi)存
//此時(shí)pgn包含的指針指向那塊堆區(qū)內(nèi)存。
//那么pgn在C++運(yùn)行時(shí)庫(kù)中銷毀時(shí),會(huì)釋放堆區(qū)內(nèi)存,不會(huì)造成內(nèi)存泄漏
//因此用detach也是安全的
}到此這篇關(guān)于C++多線程傳參的實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)C++多線程傳參內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++ 靜態(tài)成員的類內(nèi)初始化詳解及實(shí)例代碼
這篇文章主要介紹了C++ 靜態(tài)成員的類內(nèi)初始化詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02
c++之time_t和struct tm及時(shí)間戳的正確使用方式
C++中處理時(shí)間的常用數(shù)據(jù)類型有time_t和struct tm,time_t通常用來(lái)表示時(shí)間戳,即從1970年1月1日至今的秒數(shù),struct tm是一個(gè)結(jié)構(gòu)體,用來(lái)存儲(chǔ)年、月、日、時(shí)、分、秒等信息,時(shí)間戳可以通過(guò)gmtime()轉(zhuǎn)換為struct tm類型,反之亦然2024-10-10
利用C語(yǔ)言實(shí)現(xiàn)順序表的實(shí)例操作
順序表是線性表中的一種重要的數(shù)據(jù)結(jié)構(gòu),也是最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),所以他不僅是學(xué)習(xí)中的重點(diǎn),也是應(yīng)用開(kāi)發(fā)非常常用的一種數(shù)據(jù)結(jié)構(gòu)。這篇文章介紹如何利用C語(yǔ)言實(shí)現(xiàn)順序表。2016-08-08
詳解C語(yǔ)言實(shí)現(xiàn)猜數(shù)字游戲
這篇文章主要為大家介紹了C語(yǔ)言實(shí)現(xiàn)猜數(shù)字游戲,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助<BR>2022-01-01

