多線程挺簡單
c++11提供了方便的線程管理類std::thread,位於#include <thread>頭文件中,下面是個簡單的示例:
<code>void func(){ std::cout << "hello multi-thread! " << std::endl;}int main (){ for(int i = 0 ; i < 4; i++) { std::thread t(func); t.detach(); } return 0;}/<code>
上面的例子中,創建了4個線程用於輸出“hello multi-thread”。多線程初體驗 - 多線程的創建就是這麼簡單。
多線程的組成
在多線程編程中,每個應用程序至少有一個進程,每個進程至少有一個主線程。除了主線程之外,可以在一個進程中創建多個線程,每個線程都有入口函數,其中主線程的入口函數就是main函數。當入口函數執行結束時,線程隨之退出。在c++11中,使用std::thread類可以創建並啟動一個線程,該thread對象負責管理啟動的線程(執行/掛起等)。下面是使用std::thread創建線程的簡單示例:
<code>void func(int tid) { std::cout << "cur thread \\ id is [%d] !" << tid << std::endl; }std::thread t(func, tid);/<code>
上面的示例中,創建了一個thread對象,就會啟動一個線程(線程對象創建即啟動,不許額外的操作)。與第一個示例不同的一點是,這裡的入口函數需要傳入一個參數,即thread構造函數的第二個參數。
入口參數的類型
std::thread類的構造函數是使用可變參數模板實現的,也就是說,可以傳遞任意個參數,第一個參數是線程的入口函數,而後面的若干個參數是該函數的參數。其中,入口參數的類型為可調用對象(Callable Objects),一般包含以下幾種類型:
- 函數指針,即傳入函數名(c類型)
- 重載了operator()運算符的類對象,即函數對象
- lambda表達式(匿名函數)
- std::function,其實上述3種類型都可以用std::function表示,不算是單獨的一類
函數指針的示例不再贅述,關於lambda表達式和重載運算符以及std::function作為入口函數 ,下面是簡單的示例:
<code>// lambda表達式作為現成的入口函數 - 打印數字for(int i = 0; i < 4; i++){ std::thread t([i]{ std::cout << "cur number is: " << i << std::endl; }); t.detach();}/<code>
上面的例子中,啟動4個線程,並使用lambda表達式作為入口函數,實現數字打印的功能。
<code>// 重載運算符的實例作為入口函數class Test{ public: void operator()(int i) { std::cout << "cur \\ number is:" << i << std::endl; }}int main(){ for (int i = 0; i < 4; i++) { Test tmp; std::thread t(tmp, i); t.detach(); }}/<code>
把 函數對象 傳入std::thread的構造函數時,要注意一個C++的語法解析錯誤(C++'s most vexing parse)。std::thread構造函數接受的是一個臨時變量,否則就會導致語法解析錯誤,這是因為解釋器比較“笨”,將Test()解釋為了函數聲明,該函數返回一個Test對象。需要說明的是,如果重載運算符有參數,則不會出現編譯問題。知道原因,解決起來也就容易了,代碼如下:
<code>std::thread t(Test()); // 編譯出錯,可以這樣做:std::thread t{Test()}; 或者 std::thread t( (Test()) );/<code>
函數對象:定義了調用操作符的類對象,當用該對象調用此操作符時,其表現形式如同普通函數調用一般,因此取名叫函數對象。
<code>// std::function作為入口參數void add(int i, int j) { std::cout << i+j << std::endl; }std::function<void> func1 = add;std::function<void> func2 = [](int i, int j){ std::cout << i+j << std::endl; }std::thread t1(func1, num1, num2);std::thread t2(func2, num1, num2);/<void>/<void>/<code>
線程的join或者detach
一個線程啟動之後,一定要在線程對象被銷燬前確定以何種方式等待線程執行結束。等待的方式有兩中join或detach。
- join:線程啟動之後,調用join阻塞主線程,等子線程執行結束後,繼續執行主線程的指令;
- detach:線程啟動之後,調用detach不會影響主線程的執行,啟動的子線程在後臺運行;
<code>// 使用join阻塞主線程的例子int main(){auto func = [](int num){ std::cout</<code>
<code>// 使用detach等待子線程的示例int main(){auto func = [](int num){ std::cout</<code>
設置線程等待方式的注意事項
關於線程的detach等待方式,有個疑惑:會不會出現主線程執行結束,子線程仍在執行的情況?如果主線程退出,線程對象會隨之銷燬,子線程還能繼續嗎?
<code>// 使用detach等待子線程int main(){auto func = [](int num){ // Sleep(1000); for (int i = 0; i < num; i++) { std::cout</<code>
上述實驗代碼說明,使用detach時,的確會存在主線程結束後,子線程尚未結束的情況,且會導致程序崩潰!如果真是這樣,我的疑惑更大了,稍微複雜點的程序,子線程的執行時間稍微長點就可能導致上面的情況。此時貌似只能使用join,這樣的話detach是否有點雞肋,請大神不吝賜教,不勝感激!
同時也要注意到,join也不完美:使用join的難點在於,在哪裡join,不合適的join可能會導致程序串行,達不到並行的效果。如果join和detach能夠結合就好了,可以嗎?(no idea)
既然子線程可以設置為detach模式在後臺運行,需要注意:當子線程使用了局部變量,且局部變量的作用域結束,子線程尚未結束時,如果繼續使用局部變量,會出現意想不到的錯誤,並且這種錯誤很難排查。但是,線程的參數傳遞是:“默認的會將傳遞的參數以拷貝的方式複製到線程空間”,不應該出問題吧?需要自行驗證。
當使用join方式等待線程結束時,需要注意:當決定以detach方式讓線程在後臺運行時,可以在創建thread的實例後立即調用detach,這樣線程就會和thread的實例分離,即使出現了異常thread的實例被銷燬,仍然能保證線程在後臺運行。但線程以join方式運行時,需要在主線程的合適位置調用join方法,如果調用join前出現了異常,thread被銷燬,線程就會被異常所終結。為了避免異常將線程終結,或者由於某些原因,例如線程訪問了局部變量,就要保證線程一定要在函數退出前完成,就要保證要在函數退出前調用join,此時一種比較好的方法是資源獲取即初始化(RAII,Resource Acquisition Is Initialization),示例如下:
<code>class thread_guard{ thread &t;public : explicit thread_guard(thread& _t) : t(_t){} ~thread_guard() { if (t.joinable()) t.join(); } thread_guard(const thread_guard&) = delete; thread_guard& operator=(const thread_guard&) = delete;};void func(){ thread t([]{ cout << "Hello Multi-thread" <<endl>/<code>
上面的例子中,使用了std::thread類的另一個成員函數joinable(),用於判斷當前線程是否已經join。注意,無論是join還是detach方式,都只能調用一次。
線程的入口函數參數 - 引用還是傳值
入口函數的參數使用值傳遞還是引用傳遞?都可以,但是如果想在線程中對參數的修改傳遞出來(引用),你可能有失望了。因為默認的會將傳遞的參數以拷貝的方式複製到線程空間,即使參數的類型是引用,此時引用的也是線程空間中的對象,而不是初始希望改變的對象。<strong>(這也導致了我前面提到的疑惑:既然線程空間維護了參數變量的副本,即使參數變量對應的變量離開作用域被銷燬,也不會影響到線程空間的拷貝吧。)示例如下:
<code>// 下面的代碼據說g++編譯會報錯int main() { auto func = [](std::string& ref_str){ ref_str.assign("goodbey"); std::cout << "ref:" << ref_str << std::endl; } std::string orig_str = "hello"; std::thread t(func, orig_str); t.join(); std::cout << "orig:" <<orig>/<code>
如果想將更新的參數傳遞出來,可以在調用線程類構造函數的時候,
使用std::ref()。如下面修改後的代碼:<code>std::thread t(func,std::ref(orig_str));// 此時輸出為:// ref: goodbey// orig: goodbey/<code>
線程對象只能移動不可複製
線程對象之間是不能複製的,只能移動,移動的意思是,將線程的所有權在std::thread實例間進行轉移,使用std::move。示例如下:
<code>void func1(int);std::thread t1(func1, num); std::thread t2 = t1; // 編譯錯誤,不可複製std::thread t3 = std::move(t1); // 正確,將對象t1負責管理的線程轉移給t2/<code>
最後附上std::thread類的成員函數:
(constructor) Construct thread (public member function )
(destructor) Thread destructor (public member function )
operator= Move-assign thread (public member function )
get_id Get thread id (public member function )
joinable Check if joinable (public member function )
join Join thread (public member function )
detach Detach thread (public member function )
swap Swap threads (public member function )
native_handle Get native handle (public member function )
hardware_concurrency [static] Detect hardware concurrency (public static member function )
閱讀更多 rongzhenlee 的文章