C++:多线程中的小白(2)线程启动、结束、创建线程、join、detach
目錄
?
一、范例演示:線程運行的開始和結束
二、其他創建線程的手法
一、范例演示:線程運行的開始和結束
(1)thrad
(2)join()函數
(3)detach()函數
(4)joinable()函數
?
二、其他創建線程的手法
(1)用類,以及一個問題范例
(2)用lambda表達式
-----------------------------------正題----------------------------
假如我們有一個最簡單的main函數:(用它來演示線程運行的開始和結束)
int main()
{cout<<"I love you"<<endl;//實際上這個是主線程在執行,主線程從main函數返回,則整個進程執行完畢。return 0;
}
//該程序運行起來生成一個進程,該進程所屬的主線程開始自動運行起來。(當主線程執行完了之后就等于整個進程執行完畢了)
下面我們自己創建一個線程:(說明程序運行起來之后我們會有兩個線程,相當于我們這個程序有兩條線,兩條平行線在跑;
主線程從main函數開始執行,我們自己創建的另外一個線程也需要從一個函數開始執行(有時候我們也稱這個函數為初始函數),一旦這個函數運行完畢就代表這我們這個線程運行結束;)
PS:整個進程是否執行完畢的標志是什么?主線程是否執行完,如果主線程執行完畢了,那么代表著整個進程就執行完畢了;此時,一般情況下如果其他子線程還沒有執行完畢,那么這些子線程也會被操作系統強行終止(因為我整個進程都沒了),所以一般情況下我們得到一個結論:如果大家想保持子線程(此處的子線程是我們自己用代碼創建的線程)的運行狀態(意思就是想讓子線程運行),那么大家就要讓主線程一直保持運行,不要讓主線程運行完畢(主線程運行完畢的標志就是執行完return 0;這個語句,就退出了,整個進程就退出了)。但是這條規律也有例外的。
thread:是標準庫中的一個類,這個類就是用來創建線程的。
創建線程的步驟:
- 包含一個頭文件:include<stread>
- 要創建一個函數(因為之前上邊說過子線程也是從函數開始的)。
所以在上述基礎上,先創建一個函數:
//自己創建的函數也要從一個函數(初始函數)開始運行
void myprint()
{cout<<"我的線程開始執行了"<<endl;//.....干其他事cout<<“我的線程執行完畢了”<<endl;
}
int main()
{//在main中開始寫代碼,開啟線程,thread是一個類,定義一個thread類的對象mytobj,然后我傳進去構造函數的參數,我的參數直接傳進入一個myprint(myprint是個函數,函數也是一個可調用對象),std::thread mytobj(myprint);//生成了一個屬于thread類的對象。//myprint構造函數的實參,實際上是一個可調用對象(也就是說這個thread里邊的形參接收的是一個可調用對象)//1這句代碼的作用是:(1)創建出來了另外一條線,也就是創建了線程,這個線程執行的起點(入口)是myprint;(2)這個線程(函數)myprint開始執行 mytobj.join();//這個對象mytobj調用一個方法join()//2作用:阻塞主線程,并等待myprint()子線程執行完,因為myobj代表的是myprint這個子線程。
//上邊兩行就是創建一個線程并且開始執行cout<<"I love China"<<endl;//3當子線程執行完畢,再往下走,執行該語句的輸出,最終return 0;程序結束,主線程退出了。return 0;
}
結果展示如下:
??兩條平行線一起走。
因為主線程執行的是return 0;子線程只要從上到下把這個子線程的初始函數myprint里邊的代碼都執行完畢,這個子線程他自動就會執行完了。?
通過觀察結果我們可以看到,是線程先執行,I love china是后執行,兩條線走但是輸出順序是先是線程cout里邊的信息,然后才是主線程main函數中的cout內容,最后整個程序或者說是進程運行結束。(必須明確一下:現在這個代碼是有兩條線路的,有兩個線程在跑,相當于整個程序的執行有兩條線,在同時走,所以這個程序可以同時干兩個事,即使一條線被堵住了,另外一條線還是可以通行的,這就是多線程。(即使主線程被堵住了,那我子線程不耽誤我走;比如以網沒有使用多線程的時候,是從main函數開始執行的,如果我調用了其他函數,我流程就走到其他函數里邊去了,然后執行完其他函數流程再回來,一條流程。但是現在不是一條流程,現在是兩條流程在走。))
?
join()函數:
join的中文意思是加入參加,說白了就是阻塞。阻塞主線程,主線程是main函數代表的這個東西,main函數代表的執行的這條線。該函數作用:阻塞主線程讓主線程等待子線程執行完畢,然后子線程和主線程匯合,然后主線程在mytobj.join();就開始等待,然后主線程再往下走。(意思就是剛開始兩條線路一起走,當主線程遇到join函數的時候主線程就明白了我們程序開發者希望主線程等待子線程,這個時候主線程阻塞在這里,等待myprint()代表的子線程執行完,當子線程執行完畢,這個join()就執行完畢,那么阻塞的這個join就繼續往下走,也就是子線程的執行流程跟咱們當前的主線程匯合了(所以說白了這個join就是阻塞等待子線程執行完))所以join函數非常重要,決定這個整個程序的執行循序,主線程等待子線程,這是一種等待,等待匯合之后共同結束這個程序。
假如上邊把join這行注釋掉,不等待子線程了,結果如下(程序報異常了,并且輸出是混亂的):
為什么會發生異常?
子線程我剛打印一條,我還沒打印第二條呢,主線程已經執行完畢了,這時我的子線程還沒有執行完畢呢,(意思就是我子線程還在進行時,你主線程已經return 0;了,那這個程序就整個退出了,退出就導致異常。)所以結論,如果你子線程沒執行完,你主線程提前執行完了,那你這樣的程序員是不合格的,寫出來的程序也是不穩定的。
一個書寫良好的程序應該是主線程等待子線程執行完畢后自己才能最終退出。
detach():
主線程可不可以不等待子線程執行完,自己先執行完呢?可以的。這就是上邊說的例外。
傳統多線程:主線程要等待子線程執行完畢,然后自己再最后退出。
打破傳統的寫法就是detach()。
detach():中文分離的意思。意思就是主線程我不和你子線程匯合了,你子線程執行你的我主線程執行我的,你主線程也不必等我了子線程運行結束了,你可以先行結束,這并不影響我子線程的執行。這就是detach()函數的作用。
引入detach()的原因:
比如說我們創建了很多的子線程,讓主線程逐個等待子線程結束,這種編程方法不太好,所以引入了detach()。
個人建議還是采用join()這種方式,意思就是就算你子線程很多,但是我挨個等你,因為只有等這種方式,我子線程都安全退出了,這才是一種最穩定最好的編程方法。
一旦detach()之后,與這個主線程關聯的thread對象,就這個mytobj就會失去與主線程的關聯,此時這個子線程myprint這個線程就會駐留在后臺運行了,(這個后臺咱們就看不見摸不著了,主線程跟該子線程失去聯系)這個子線程就相當于被C++運行時庫接管了(運行時庫就是系統這玩意,不用管他,自會有人幫我們去干),當這個子線程執行完成后,由運行時庫負責清理該線程相關的資源(主要包括變量內存釋放等)
將上述的代碼用detach替換join。
//自己創建的函數也要從一個函數(初始函數)開始運行
void myprint()
{cout<<"我的線程開始執行了"<<endl;//.....干其他事cout<<“我的線程執行完畢了1”<<endl;cout<<“我的線程執行完畢了2”<<endl;cout<<“我的線程執行完畢了3”<<endl;cout<<“我的線程執行完畢了4”<<endl;cout<<“我的線程執行完畢了5”<<endl;cout<<“我的線程執行完畢了6”<<endl;cout<<“我的線程執行完畢了7”<<endl;cout<<“我的線程執行完畢了8”<<endl;cout<<“我的線程執行完畢了9”<<endl;cout<<“我的線程執行完畢了10”<<endl;
}
int main()
{//在main中開始寫代碼,開啟線程,thread是一個類,定義一個thread類的對象mytobj,然后我傳進去構造函數的參數,我的參數直接傳進入一個myprint(myprint是個函數,函數也是一個可調用對象),std::thread mytobj(myprint);//生成了一個屬于thread類的對象。//myprint構造函數的實參,實際上是一個可調用對象(也就是說這個thread里邊的形參接收的是一個可調用對象)//1這句代碼的作用是:(1)創建出來了另外一條線,也就是創建了線程,這個線程執行的起點(入口)是myprint;(2)這個線程(函數)myprint開始執行 mytobj.detach();cout<<"我的線程開始執行了主線程收尾,最終最線程安全正常退出!"<<endl;//3當子線程執行完畢,再往下走,執行該語句的輸出,最終return 0;程序結束,主線程退出了。return 0;
}
結果展示:
結果只打印主線程中的一條,子線程中的一個都沒打印。進程就退出了,進程退出表明我們這個線程執行完了,
把上述代碼再改一下:
//自己創建的函數也要從一個函數(初始函數)開始運行
void myprint()
{cout<<"我的線程開始執行了"<<endl;//.....干其他事cout<<“我的線程執行完畢了1”<<endl;cout<<“我的線程執行完畢了2”<<endl;cout<<“我的線程執行完畢了3”<<endl;cout<<“我的線程執行完畢了4”<<endl;cout<<“我的線程執行完畢了5”<<endl;cout<<“我的線程執行完畢了6”<<endl;cout<<“我的線程執行完畢了7”<<endl;cout<<“我的線程執行完畢了8”<<endl;cout<<“我的線程執行完畢了9”<<endl;cout<<“我的線程執行完畢了10”<<endl;
}
int main()
{//在main中開始寫代碼,開啟線程,thread是一個類,定義一個thread類的對象mytobj,然后我傳進去構造函數的參數,我的參數直接傳進入一個myprint(myprint是個函數,函數也是一個可調用對象),std::thread mytobj(myprint);//生成了一個屬于thread類的對象。//myprint構造函數的實參,實際上是一個可調用對象(也就是說這個thread里邊的形參接收的是一個可調用對象)//1這句代碼的作用是:(1)創建出來了另外一條線,也就是創建了線程,這個線程執行的起點(入口)是myprint;(2)這個線程(函數)myprint開始執行 mytobj.detach();//這個后邊不能再用join函數了。cout<<"I Love China1"<<endl;cout<<"I Love China2"<<endl;cout<<"I Love China3"<<endl;cout<<"I Love China4"<<endl;cout<<"I Love China5"<<endl;return 0;
}
運行結果
因為現在detach了,我主線程走我主線程的,子線程走我子線程的,我兩條路咱倆自己執行各自的誰也別耽誤誰。
如果你再執行一次,你發現輸出結果又不一樣了。
主線程執行完I Love china 5之后主線程就退出了,退出去之后導致本來主線程如果不退出之后,子線程會把cout的所有語句輸出,但是我們卻發現主線程一旦退出,子線程就輸出不出來了,(也就是78910語句還沒來得及輸出到屏幕呢,主線程執行到return 0了,執行完return 0主線程就退出了,那整個進程就退出了,進程退出之后子線程后邊的就輸出不到屏幕上來了,因為你這個東西使用了detach你被C++運行時庫接管了),所以只要主線程一結束,你子線程顯示的內容就輸出不到屏幕上來了。(detach因為主線程退出了,導致子線程的輸出就中斷了,隨著主線程的退出,整個進程的退出,這個線程也轉入到后臺去執行了,本來他就是在后臺執行的,因為你從detach的那一時刻起,整個線程就是在后臺執行的,只不過是你進程沒執行完,我線程可以通過輸出語句往你屏幕上打印輸出信息罷了,你一旦進程退出了,你就沒有辦法在屏幕上再打印出信息來而已,你就看不到后續的myprint打印出來的信息,所以detach這個感覺就是detach會導致這個線程myprint失去咱們的控制,我們管不了它了,所以通常情況下更推薦用join等待的方法,這個主線程等子線程;當然這個不是絕對的,如果子線程要干的活和這個主線程沒有什么關系,大家其實是可以使用detach的,注意,你一旦調用detach了,那么這個線程myprint就跑到后臺去了,你就管不著它了,那你也不能再join回來了,就不能說再用join了,后邊不能再調用join這個函數了(一旦detach就不能再用join否則系統會報異常)。)
joinable()
joinable函數功能:判斷是否可以成功使用join()或者detach(),返回true(表示可以join或者可以detach)或者false(不能join或者不可以detach),因我們在創建完線程后要么使用join要么使用detach,那么我們有可能中途需要判斷哪一個地方能不能使用join或者detach,這個時候我們就用到joinable了,因為以后寫復雜代碼得時候可能會需要判斷是否join過或者是否detach過等等,這個函數就發揮作用了。比如下邊:
//自己創建的函數也要從一個函數(初始函數)開始運行
void myprint()
{cout<<"我的線程開始執行了"<<endl;//.....干其他事cout<<“我的線程執行完畢了”<<endl;
}
int main()
{std::thread mytobj(myprint);//mytobj.joinable();//此處調用joinable()函數返回的就是true。//因為只有創建線程這一行,還沒有調用過join和detach。//所以使用一個if語句進行判斷:if(mytobj.joinable()){cout<<"1:joinable()==true"<<endl;}//因為只有創建線程這一行,還沒有調用過join和detach,If語句里邊就可以join()或者detach();elae{cout<<"1:joinable()==false"<<endl;}mytobj.join();//mytobi.joinable()//此處調用joinable()函數返回的就是false。因為前邊已經調用過join函數了if(mytobj.joinable()){cout<<"2:joinable()==true"<<endl;}elae{cout<<"2:joinable()==false"<<endl;}//此處已經調用過了join()函數了。cout<<"I love China"<<endl;return 0;
}
總結:你detach后不能再detach或者join了;同樣你join之后也不能再join或者detach了。
二、其他創建線程的手法
上述創建線程是從一個函數他是可調用對象創建一個線程,然后這個線程就從myprint函數作為入口點,開始執行。之前講過thread這個東西是接受一個可調用對象作為參數,來創建線程的,那函數它是一個可調用對象,但是還有其他可調用對象啊。
(1)用類對象作為一個可調用對象
首先得定義一個類。(如果一個類作為可調用對象的話,那么我們這個類中必須得有public,)
這個類就是一個可調用對象,如果用這個類去創建線程的話,那么這個線程的執行入口點應該就是operator這個函數了
//創建一個類
class TA
{
public:void operator()();//重載括號,第二個()里邊是參數,這里先不帶參數{cout<<"我的線程operator()開始執行了"<<endl;//..執行其他事cout<<"我的線程operator()結束執行了"<<endl;}//沒有這個圓括號重載的函數,那么改類就不是一個可調用對象
}
int main()
{TA ta;//首先生成一個類對象,(最好不要使用匿名對象,如果使用匿名對象下邊應該傳入的是TA(),沒試過不知道對不對)thread mytobj2(ta);//ta是個可調用對象(上一個例子傳入的是函數,現在是類對象)mytobj2.join();//等待子線程執行結束cout<<"I Love China"<<endl;return 0;
}
結果:
給該類增加一個構造函數:
//創建一個類
class TA
{
public:int &m_i;//定義成員變量,為int,但是它是一個引用//TA(int &i);//構造函數,參數是引用TA(int &i):m_i(i){}//構造函數帶參數void operator()();//重載括號,第二個()里邊是參數,這里先不帶參數{cout<<"m_i1的值為:"<<m_i<<endl;//..執行其他事cout<<"m_i2的值為"<<m_i<<endl;cout<<"m_i3的值為"<<m_i<<endl;cout<<"m_i4的值為"<<m_i<<endl;}//沒有這個圓括號重載的函數,那么改類就不是一個可調用對象
}
int main()
{int myi=6;TA ta(myi);//這樣才能創建ta這個對象thread mytobj3(ta);//ta是被賦值的,相當于又生成一個新對象復制到這個線程中去的//ta是個可調用對象(上一個例子傳入的是函數,現在是類對象)mytobj3.detach();//等待子線程執行結束cout<<"I Love China"<<endl;return 0;
}
該結果每次執行結果不一樣。
假設這個主線程他先執行完,也能是子線程后執行完的,那如果主線程先執行完了,你子線程后執行完的,如果出現這個情況的話,主線程執行完了,子線程還在后臺執行這呢,但是不要忘記,我這個子線程打印的m_i他是一個引用,它綁著的是i,這個i綁著的是myi,myi他是主線程的局部變量,當主線程執行結束后,myi變量的內存就已經被回收了,被銷毀了,那你主線程執行完,那你這個myi就被銷毀了,這段內存被回收了,那你子線程沒執行完,仍然在打印這段內存的內容(被回收的這段內存的內容),那么就會產生不可預料的結果了,所以最好join等子線程執行完成。(假設主線程運行結束,那么我這個ta他也是一個局部變量,我們用局部對象構造一個線程,那我這個主線程執行完了,那我這個ta不就無效了嗎?如果ta無效的話,那還能執行這個ta里邊的可調用圓括號不就不能執行了嗎?但是為什么這里用ta沒問題?還有一個問題,一旦調用了detach()那我主線程執行結束了,我這里用的ta這個對象還在嗎?這個對象你一旦主線程結束了,這個ta對象肯定是不在了,因為主線程結束了嘛,他是個局部變量嘛,此時這個對象不在了,如果這個對象不在了還能調用這個對象的成員函數嗎?答:因為這個對象實際上是被復制到線程中去的,執行完主線程后ta會被銷毀,但是所復制的ta對象依舊存在,所以只要你這個TA類對象里沒有引用,沒有指針,那么就不會產生問題,因為你這個對象是被復制到線程中去的,)
(2)用lambda表達式創建
int main()
{auto mylamthread=[]{//這個就是線程入口cout<<"我的線程3開始執行了"<<endl;//...干其他事cout<<"我的線程3執行結束了"<<endl;};thread mytobj4(mylamthread);//mylamthread是個可調用對象mytobj4.join();cout<<"I Love China"<<endl;return 0;
}
執行結果:
?
總結
以上是生活随笔為你收集整理的C++:多线程中的小白(2)线程启动、结束、创建线程、join、detach的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PCL调错:(3)error C2589
- 下一篇: Eigen:C++中Eigen库的安装与