c++11 Thread庫寫多線程程序


一個簡單的使用線程的Demo

       c++11提供了一個新的頭文件<thread>提供了對線程函數的支持的聲明(其他數據保護相關的聲明放在其他的頭文件中,暫時先從thread頭文件入手吧),寫一個多線程的程序需要引用這個新的頭文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <thread>
 
void  fun()
{
    std::cout <<  "A new thread!"  << std::endl;
}
 
int  main()
{
     std:: thread  t(fun);
     t.join();
     std::cout <<  "Main thread!"  << std::endl;
}

  這樣的demo就是一個簡單的多線程的應用了。其輸出如下:

1
2
A new thread!
Main thread!

因此我們可以猜測到它的執行流大致是這樣的:

那么程序的執行流是如何從main()轉去執行fun()的呢,下面我們先看看thread這個類。

線程的啟動

      我的環境是CentOS7 + g++4.8.3 頭文件/usr/include/c++/4.8.3/thread中有thread類的完整聲明(我的windows環境是win8.1+vs2013,在默認安裝的情況下thread頭文件的路徑是C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include\thread)。代碼太長,我就不貼出來了。c++線程庫通過構造一個線程對象,來啟動一個線程。那么我們就先來看一下thread的構造函數:

以下貼出thread類的代碼均是出自GNU的實現版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class  thread
{
     ...
public :
     thread ()  noexcept  default ;
     thread ( thread &) =  delete ;
     thread ( const  thread &) =  delete ;
     thread ( thread && __t)  noexcept
     { swap(__t); }
     template < typename  _Callable,  typename ... _Args>
     explicit  thread (_Callable&& __f, _Args&&... __args)
     {
         _M_start_thread(_M_make_routine(std::__bind_simple(
         std::forward<_Callable>(__f),
         std::forward<_Args>(__args)...)));
      }
     ...
};

這幾行代碼里邊,卻有着大量的有關c++11特性的內容,右值引用、noexcept、=delete、以及可調用對象這些不是本篇博客要關注的點,因此就不詳細的展開了。我們主要來看這個構造函數模板,

template<typename _Callable, typename... _Args>
explicit thread(_Callable&& __f, _Args&&... __args);

可以清楚的看見它把參數都交給了std::__bind_simple()處理了,而對於std::__bind_simple()的定義在其頭文件<functional>中,和std::bind()的用法一樣,具體區別建議還是看看頭文件比較好,這里我就多個事,順手就貼出來了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template < typename  _Callable,  typename ... _Args>
typename  _Bind_simple_helper<_Callable, _Args...>::__type
__bind_simple(_Callable&& __callable, _Args&&... __args)
{
     typedef  _Bind_simple_helper<_Callable, _Args...> __helper_type;
     typedef  typename  __helper_type::__maybe_type __maybe_type;
     typedef  typename  __helper_type::__type __result_type;
     return  __result_type(
         __maybe_type::__do_wrap( std::forward<_Callable>(__callable)),
         std::forward<_Args>(__args)...);
}
    
template < typename  _Result,  typename  _Func,  typename ... _BoundArgs>
inline  typename  _Bindres_helper<_Result, _Func, _BoundArgs...>::type
bind(_Func&& __f, _BoundArgs&&... __args)
{
     typedef  _Bindres_helper<_Result, _Func, _BoundArgs...> __helper_type;
     typedef  typename  __helper_type::__maybe_type __maybe_type;
     typedef  typename  __helper_type::type __result_type;
     return  __result_type(__maybe_type::__do_wrap(std::forward<_Func>(__f)),
        std::forward<_BoundArgs>(__args)...);
}

功力有限std::bind()具體實現我就不深究了,有機會我在研究研究。但是不難看出,這兩個函數的作用大體上就是封裝一個函數及其參數,返回一個__type類。thread在構造一個新的對象時,便是傳入了一個__type對象給_M_start_thread()實現啟動一個線程的。為什么要這樣做呢?我們可以認為這是由於OS的實現(我也是網上聽說,如果你知道答案,不妨告訴我),用過Linux上的線程庫pthread的應該對pthread_create()中的start_routine參數有印象,它是一個函數指針,其對應的函數原型如下:

void* (*start_routine) (void*);

這樣就縮小了兩者之間的差異,剩下的事就只要把__type的地址傳進去就可以了。由於使用的這樣的實現,std::thread()創建一個新的線程可以接受任意的可調用對象類型(帶參數或者不帶參數),包括lambda表達式(帶變量捕獲或者不帶),函數,函數對象,以及函數指針。

      上面我們寫了一個不帶參數的demo,現在我們就創建包含參數和捕獲的lambda表達式看看是否真的是這樣,demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <thread>
#include <iostream>
 
int  main()
{
     int  n1 = 500;
     int  n2 = 600;
     std:: thread  t([&]( int  addNum){
         n1 += addNum;
         n2 += addNum;
     },500);
     t.join();
     std::cout << n1 <<  ' '  << n2 << std::endl;
}

得到了預期結果:

1
2
[thread]main
1000 1100
線程結束

      在啟動了一個線程(創建了一個thread對象)之后,當這個線程結束的時候(std::terminate()),我們如何去回收線程所使用的資源呢?thread庫給我們兩種選擇:1.加入式(join()) 2.分離式(detach())。值得一提的是,你必須在thread對象銷毀之前做出選擇,這是因為線程可能在你加入或分離線程之前,就已經結束了,之后如果再去分離它,線程可能會在thread對象銷毀之后繼續運行下去。

Note that you only have to make this decision before the std::thread object is destroyed—the thread itself may well have finished long before you join with it or detach it, and if you detach it,then the thread may continue running long after the std::thread object is destroyed.-------《C++ Concurrency In Action》 2.1.1

      join()字面意思是連接一個線程,意味着主動地等待線程的終止,上面的例子我都是使用了join()的方式。join()是這樣工作的,在調用進程中join(),當新的線程終止時,join()會清理相關的資源(any storage associated with the thread),然后返回,調用線程再繼續向下執行。正是由於join()清理了線程的相關資源,因而我們之前的thread對象與已銷毀的線程就沒有關系了,這意味着一個線程的對象每次你只能使用一次join(),當你調用的join()之后joinable()就將返回false了。光靠文字還是略顯蒼白的,肯定還是代碼更加直觀:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <thread>
 
void  foo()
{
     std::this_thread::sleep_for(std::chrono::seconds(1));
}
  
int  main()
{
     std:: thread  t(foo);
     std::cout <<  "before joining,joinable="  << std::boolalpha << t.joinable() << std::endl;
     t.join();
     std::cout <<  "after joining, joinable="  << std::boolalpha << t.joinable() <<  '\n' ;
}

運行結果:

1
2
3
[thread]main
before joining,joinable= true
after joining, joinable= false

      第二種方式是分離式,對應的函數是detach()。detach這個詞的意思是分離的意思,對一個thread對象使用detach()意味着從調用線程分理出這個新的線程,我們稱分離的線程叫做守護線程(daemon threads)。之后也就不能再與這個線程交互。打個不太恰當的比方,就像是你和你女朋友分手(你可能說我好壞,為什么不說是我和我的女朋友?因為我沒有女朋友啊,哈哈,看我多機智。),那之后你們就不會再有聯系(交互)了,而她的之后消費的各種資源也就不需要你去埋單了(清理資源)。既然沒有交互,也就談不上join()了,因此調用joinable()必然是返回false。分離的線程會在后台運行,其所有權(ownership)和控制權將會交給c++運行庫。同時,C++運行庫保證,當線程退出時,其相關資源的能夠正確的回收。

      分離的線程,大致上是這樣執行的,它運行結束后,不再需要通知調用它的線程:

線程的標識符

      在thread類中有一個叫做_M_id的私有變量用來標識線程,其類型是std::thread::id,在GNU上實現如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class  thread
{
     ...
     class  id
     {
       native_handle_type    _M_thread;
     public :
       id()  noexcept  : _M_thread() { }
       explicit
       id(native_handle_type __id) : _M_thread(__id) { }
     private :
       friend  class  thread ;
       friend  class  hash< thread ::id>;
       friend  bool
       operator==( thread ::id __x,  thread ::id __y)  noexcept
       return  __gthread_equal(__x._M_thread, __y._M_thread); }
       friend  bool
       operator<( thread ::id __x,  thread ::id __y)  noexcept
       return  __x._M_thread < __y._M_thread; }
       <br>       template < class  _CharT,  class  _Traits>
       friend  basic_ostream<_CharT, _Traits>&
       operator<<(basic_ostream<_CharT, _Traits>& __out,  thread ::id __id);
     };
 
private :
     id              _M_id;
public :
     thread ::id
     get_id()  const  noexcept
     {
         return  _M_id;
     }
     ...
};

      代碼還是比較清晰的,很明顯我們可以通過std::this_thread::get_id()這個函數獲取線程的標識符,由於上面的代碼中thread::id類中重載了operator “<<”運算符,因此我們可以對id類型進行輸出。同時,當一個thread對象並沒有關聯一個線程的時候(可能thread對象是默認初始化的或者初始化的線程已經運行結束被join()或者是線程已經detach()),這時候get_id()將返回默認構造的id對象,意味着這個thread對象不存在關聯的線程,輸出可能像是這樣的:“thread::id of a non-executing thread”。與此同時,我們也可以在當前的線程中獲取當前線程的線程標識符,方法比較簡單直接調用std::this_thread::get_id()即可。

      現在,我們寫一個使用標准輸出嘗試輸出線程id的demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <thread>
 
void  fun()
{
     std::cout << std::this_thread::get_id() << std::endl;
}
 
int  main()
{
     std:: thread  t(fun);
     std::cout << t.get_id() << std::endl;
     t.join();
}

  其輸出結果是一個15位的整數,具體取決於實現,當然具體怎么實現並無區別,我們只要關心它可以作為標識線程的一種手段:

1
2
3
[thread]main
140302328772352
140302328772352

      同時,std::thread::id中還重載了operator==,這樣就允許我們去比較兩個線程是否相等(是否是同一個線程),比如我們需要給不同的線程分配任務或者限制某個線程的操作,id類型實現了這樣的比較運算給了我們編程時極大的便利。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM