C++多線程

原地址: http://blog.csdn.net/hujingshuang/article/details/70208443

C++多線程支持庫(Thread support library)

        C++的內置支持包括thread(線程),mutual exclusion(互斥),condition variables(條件變量)和future等。

        頭文件有:

                <thread> 

                <mutex> 

                <condition_variable> 

                <future>

        先來說頭文件<thread>,這裏面有兩個東西,一個是thread類(class thread),一個是this_thread命名空間(namespace this_thread,該命名空間中有幾個有用的函數),另外類thread中還有個內部id(class id)。

多線程舉例

        通過一段多線程的代碼,我們一步步來揭開thread的面紗,代碼如下:

[cpp] view plain copy
  1. #include <iostream>  
  2. #include <thread>  
  3.   
  4. using namespace std;  
  5.   
  6. void task_one() {  
  7.     for (int i = 0; i < 10; i++) {  
  8.         cout << this_thread::get_id() << '\t' << i << endl;  
  9.         this_thread::sleep_for(chrono::milliseconds(5));    // 休眠5ms  
  10.     }  
  11. }  
  12.   
  13. void task_two(int n) {  
  14.     for (int i = 0; i < n; i++) {  
  15.         cout << this_thread::get_id() << '\t' << i << endl;  
  16.         this_thread::sleep_for(chrono::milliseconds(10));   //休眠10ms  
  17.     }  
  18. }  
  19.   
  20. int main() {  
  21.     int n = 20;  
  22.   
  23.     thread t1(task_one);  
  24.     thread t2(task_two, n);  
  25.   
  26.     t1.join();  
  27.     t2.join();  
  28.   
  29.     return 0;  
  30. }  
        上述代碼中,一共存在三個線程,t1,t2和程序主線程(也就是執行main的那個線程)。線程t1、t2的任務分別是執行task_one、task_two(也就是兩個函數),在各自的線程中打印線程id及循環量i。

        另外代碼t1.join()和t2.join()在main函數中,也就是說在主線程中,表示將主線程與線程t1、t2相結合。這樣一來,主線程會阻塞,直到線程t1、t2執行完畢,主線程纔會執行後面的代碼。

線程的join與detach

        在這裏要說明線程的兩種狀態:在任何一個時刻,線程是結合 或 分離 狀態:

                1、一個結合狀態的線程能夠被其他線程回收資源和殺死,在被其他線程回收之前,它所佔有的資源是不釋放的;

                2、一個分離狀態的線程是不能被其他線程回收或殺死的,它所佔有的資源會在該線程執行完畢後由系統自動釋放

        線程的結合和分離狀態決定了一個線程以什麼樣的方式來終止自己,在默認情況下線程是非分離的狀態。

        OK,大家有了上述的概念,我們就可以開始看<thread>源碼了,代碼也不是很長,大家預覽一遍有個印象就可以了,分析在後面。(VS2013源碼在這裏:\Microsoft Visual Studio 12.0\VC\include\thread)

[cpp] view plain copy
  1. // 管理線程的類  
  2. class thread {  
  3. public:  
  4.     class id;                   // 內部類,後面會分析  
  5.     typedef void *native_handle_type;  
  6.   
  7.     thread() _NOEXCEPT {        // 構造函數,空線程  
  8.         _Thr_set_null(_Thr);    // 宏定義,原型爲:#define _Thr_set_null(thr) (thr._Id = 0)  
  9.     }  
  10.   
  11.     template<class _Fn, class... _Args>  
  12.     explicit thread(_Fn&& _Fx, _Args&&... _Ax) {    // 帶參模板構造函數_Fx(_Ax...)  
  13.         _Launch(&_Thr, _STD bind(_Decay_copy(_STD forward<_Fn>(_Fx)), _Decay_copy(_STD forward<_Args>(_Ax))...));  
  14.     }  
  15.   
  16.     ~thread() _NOEXCEPT {       // 析構函數  
  17.         if (joinable())         // 線程是可結合的,析構異常(也就是說只能析構不可結合的線程)  
  18.             _XSTD terminate();  // terminate會調用abort()來終止程序  
  19.     }  
  20.   
  21.     thread(thread&& _Other) _NOEXCEPT : _Thr(_Other._Thr) {     // 拷貝構造函數,調用move  
  22.         _Thr_set_null(_Other._Thr);  
  23.     }  
  24.   
  25.     thread& operator=(thread&& _Other) _NOEXCEPT {  // 賦值函數,調用move  
  26.         return (_Move_thread(_Other));  
  27.     }  
  28.   
  29.     thread(const thread&) = delete;                 // 禁用 拷貝構造函數  
  30.     thread& operator=(const thread&) = delete;      // 禁用 賦值函數  
  31.   
  32.     void swap(thread& _Other) _NOEXCEPT {           // 交換兩線程  
  33.         _STD swap(_Thr, _Other._Thr);  
  34.     }  
  35.   
  36.     bool joinable() const _NOEXCEPT {               // 若線程可結合程,返回 true;否則,返回flase  
  37.         return (!_Thr_is_null(_Thr));               // 宏定義,原型爲:#define _Thr_is_null(thr) (thr._Id == 0)  
  38.     }  
  39.   
  40.     void join();                                    // 線程結合,阻塞的  
  41.   
  42.     void detach() {                                 // 線程分離  
  43.         if (!joinable())                            // 若線程是不可結合的,則異常  
  44.             _Throw_Cpp_error(_INVALID_ARGUMENT);  
  45.         _Thrd_detachX(_Thr);  
  46.         _Thr_set_null(_Thr);  
  47.     }  
  48.   
  49.     id get_id() const _NOEXCEPT;                    // 獲取線程唯一 id  
  50.   
  51.     static unsigned int hardware_concurrency() _NOEXCEPT {      // 返回硬件線程上下文數量  
  52.         return (::Concurrency::details::_GetConcurrency());  
  53.     }  
  54.   
  55.     native_handle_type native_handle() {            // 以 void* 形式返回線程的 Win32 句柄  
  56.         return (_Thr._Hnd);  
  57.     }  
  58.   
  59. private:  
  60.     thread& _Move_thread(thread& _Other) {          // move from _Other  
  61.         if (joinable())  
  62.             _XSTD terminate();  
  63.         _Thr = _Other._Thr;  
  64.         _Thr_set_null(_Other._Thr);  
  65.         return (*this);  
  66.     }  
  67.   
  68.     _Thrd_t _Thr;            // 私有成員變量,_Thrd_t是一個結構體,後面會分析  
  69. };  

源碼分析

成員變量

        先來看thread類中唯一的一個私有成員變量,在代碼中提到了它是一個結構體,看下面的定義就明瞭了:

[cpp] view plain copy
  1. _Thrd_t _Thr; //其實_Thrd_t 是類型的別名  
  2.   
  3. typedef _Thrd_imp_t _Thrd_t;    // 而_Thrd_imp_t是一個結構體  
  4.   
  5. typedef struct {    /* 線程 Win32 標識符 */  
  6.     void *_Hnd;     /* Win32 句柄 */  
  7.     unsigned int _Id;    // 線程id  
  8. } _Thrd_imp_t;  
        到這裏,我想大家心中終於有點着落了吧。

成員方法

        現在來剖析剩下的thread方法。


1、thread::joinable()方法,其定義如下:

[cpp] view plain copy
  1. bool joinable() const _NOEXCEPT {       // 若線程可結合程,返回 true;否則,返回flase  
  2.     return (!_Thr_is_null(_Thr));       // 宏定義,原型爲:#define _Thr_is_null(thr) (thr._Id == 0)  
  3. }  

該方法判斷線程是否可結合,實質就是判斷線程id是否爲0。


2、thread::join()方法,其定義如下:

[cpp] view plain copy
  1. inline void thread::join(){ // join thread  
  2.     if (!joinable())            // 線程不可結合  
  3.         _Throw_Cpp_error(_INVALID_ARGUMENT);  
  4.     if (_Thr_is_null(_Thr))     // 空線程  
  5.         _Throw_Cpp_error(_INVALID_ARGUMENT);  
  6.     if (get_id() == _STD this_thread::get_id()) // 線程不能與自己結合  
  7.         _Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR);  
  8.     if (_Thrd_join(_Thr, 0) != _Thrd_success)   // 線程結合(_Thrd_join()是join方法的核心),是阻塞的  
  9.         _Throw_Cpp_error(_NO_SUCH_PROCESS);  
  10.     _Thr_set_null(_Thr);        // 設置線程id爲0  
  11. }  
由上述代碼和註釋可以知道,在以下幾種情況下,線程是不可結合的:

        ① 線程已經join()過了;

        ② 線程爲空線程;

        ③ 單個的線程,也就是線程自己與自己;

如果一個可結合的線程經過join後(等線程執行完畢後),會將線程id置爲0。


3、thread::detach()方法,定義如下:

[cpp] view plain copy
  1. void detach() { // detach thread  
  2.     if (!joinable())        // 線程不可結合  
  3.         _Throw_Cpp_error(_INVALID_ARGUMENT);  
  4.     _Thrd_detachX(_Thr);    // 線程分離(detach的核心)  
  5.     _Thr_set_null(_Thr);    // 設置線程id爲0  
  6. }  

好了,這裏幾個比較重要的方法和概念就分析完畢了。接下來介紹一下構造函數、析構函數及其他函數,最後會來總結一下。


4、析構函數,定義如下:

[cpp] view plain copy
  1. ~thread() _NOEXCEPT {       // 析構函數  
  2.     if (joinable())         // 線程是可結合的,析構異常(也就是說只能析構不可結合的線程,即id爲0線程;id不爲0的線程不能析構)  
  3.         _XSTD terminate();  // terminate會調用abort()來終止程序  
  4. }  

這個如果存在疑問,待會兒請看後面的總結。


5、構造函數

[cpp] view plain copy
  1. thread() _NOEXCEPT {        // 構造函數,空線程  
  2.     _Thr_set_null(_Thr);    // 宏定義,原型爲:#define _Thr_set_null(thr) (thr._Id = 0)  
  3. }  
  4.   
  5. template<class _Fn, class... _Args>  
  6. explicit thread(_Fn&& _Fx, _Args&&... _Ax) {    // 帶參模板構造函數_Fx(_Ax...)  
  7.     _Launch(&_Thr, _STD bind(_Decay_copy(_STD forward<_Fn>(_Fx)), _Decay_copy(_STD forward<_Args>(_Ax))...));  
  8. }  
  9.   
  10. thread(thread&& _Other) _NOEXCEPT : _Thr(_Other._Thr) {     // 拷貝構造函數,調用move  
  11.     _Thr_set_null(_Other._Thr);  
  12. }  
  13.   
  14. thread& operator=(thread&& _Other) _NOEXCEPT {  // 賦值函數,調用move  
  15.     return (_Move_thread(_Other));  
  16. }  
  17.   
  18. thread(const thread&) = delete;                 // 禁用 拷貝構造函數  
  19. thread& operator=(const thread&) = delete;      // 禁用 賦值函數  

對於構造函數和賦值函數,下面舉幾個列子大家就明白了:

[cpp] view plain copy
  1. int n = 20;  
  2. thread t1, t2;          // 正確,空線程  
  3.   
  4. // 帶參的構造函數,先寫線程執行的函數,後面有多少參數就跟多少個  
  5. thread t3(task_one);    // 正確,0個參數  
  6. thread t4(task_two, n); // 正確,1個參數  
  7.   
  8. thread t5(t3);          // 錯誤,使用了被禁用的拷貝構造函數  
  9. thread t6 = t4;         // 錯誤,使用了被禁用的賦值函數  
  10.   
  11. thread t7(move(t3));    // 正確,使用move,t7與t3功能相同,但t3被move之後變成了空線程  
  12. thread t8 = move(t4);   // 正確,使用move,t8與t3功能相同,但t3被move之後變成了空線程  


6、其他方法

        ① thread::get_id()方法,獲取線程id;其定義如下:

[cpp] view plain copy
  1. inline thread::id thread::get_id() const _NOEXCEPT {  
  2.     return (id(*this));  
  3. }  

        由於之前提到過id是個內部類,這個後面再分析。


        ② thread::swap()方法,線程交換;其定義如下:

[cpp] view plain copy
  1. void swap(thread& _Other) _NOEXCEPT {  
  2.     _STD swap(_Thr, _Other._Thr);  
  3. }  

[cpp] view plain copy
  1. template<class _Ty> inline  
  2. void swap(_Ty& _Left, _Ty& _Right) _NOEXCEPT_OP(is_nothrow_move_constructible<_Ty>::value && is_nothrow_move_assignable<_Ty>::value) {  
  3.     _Ty _Tmp = _Move(_Left);  
  4.     _Left = _Move(_Right);  
  5.     _Right = _Move(_Tmp);  
  6. }  


        ③ thread::hardware_concurrency()方法,這是個靜態方法,返回的是硬件線程上下文數量;其定義如下:

[cpp] view plain copy
  1. static unsigned int hardware_concurrency() _NOEXCEPT {  
  2.     return (::Concurrency::details::_GetConcurrency());  
  3. }  

       

        ④ thread::native_handle()方法,獲取線程的win32句柄;其定義如下:

[cpp] view plain copy
  1. native_handle_type native_handle() {    // 其中:typedef void *native_handle_type;  
  2.     return (_Thr._Hnd);  
  3. }  


最後還有一個私有方法,這個方法在拷貝構造函數(使用move)中調用。

        ⑤ thread::_Move_thread()方法,其定義如下:

[cpp] view plain copy
  1. thread& _Move_thread(thread& _Other) {  // move from _Other  
  2.     if (joinable())  
  3.         _XSTD terminate();  
  4.     _Thr = _Other._Thr;  
  5.     _Thr_set_null(_Other._Thr);    //線程id置爲0  
  6.     return (*this);  
  7. }  


總節

        最後,我們來總結一下,其實重點要理解的就是線程的join、detach、joinable三者的關係:

        我們再從thread的析構函數~thread()入手分析。

[cpp] view plain copy
  1. ~thread() _NOEXCEPT {       // 析構函數  
  2.     if (joinable())         // 線程是可結合的,析構異常(也就是說只能析構不可結合的線程)  
  3.         _XSTD terminate();  // terminate會調用abort()來終止程序  
  4. }  
        其實析構函數裏面只進行了判斷,並沒有析構什麼,因爲thread成員變量不存在用new或malloc進行內存分配的指針或數組,所以析構函數裏不做資源釋放工作。那麼爲什麼只能析構不可結合的線程呢?

        這裏還是以博客最開始的代碼來分析,有主線程、t1、t2三個線程,如下。

[cpp] view plain copy
  1. int main() {  
  2.     int n = 20;  
  3.   
  4.     thread t1(task_one);  
  5.     thread t2(task_two, n);  
  6.   
  7.     t1.join();  
  8.     t2.join();  
  9.   
  10.     cout << "main thread" << endl;  
  11.     return 0;  
  12. }  

        我們可以總結一下線程不可結合(即joinable()爲false)的幾種情況:

               ① 空線程;

                ② move後的線程(即move(t),則t是不可結合的);

               ③ join後的線程;

                ④ detach後的線程;

        在實例化了t1、t2對象之後,它們的狀態默認都是可結合的,如果現在直接調用它們的析構函數來析構它們,那麼在析構的時候線程處於什麼狀態呢?是執行完了嗎?還是正在執行呢?注意,如果一個在沒有結合(join)的情況下,就算它先於主線程執行完畢,其id依然是不爲0的。所以我們是不能確定其狀態的,所以我們只能析構明確了id爲0的線程。因爲id爲0的線程要麼已經執行完畢,要麼是空線程,要麼是分離後的線程。

        另外,一個線程分離(detech)後,該線程對象邊便不能控制該線程,而是交由系統接管。


        今天就到這裏。上面有些理解都是個人的理解和看法,或許有詞不達意或者錯誤的地方,希望大家能夠多多指教,謝謝!


補充說明:

voidFunc3() noexcept;

noexcept的功能相當於上面的throw(),表示函數不會拋出異常。如果noexcept修飾的函數拋出了異常,編譯器可以選擇直接調用std::terminate()終止程序運行。noexcept比throw()效率高一些。

voidFunc4() noexcept(常量表達式);

如果常量表達式的結果爲true,表示該函數不會拋出異常,反之則有可能拋出異常。不帶常量表達式的noexcept相當於noexcept(true)。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章