02.26 c++11多線程

多線程挺簡單

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 )


分享到:


相關文章: