1. 程式人生 > >漫談c++11 Thread庫之使寫多執行緒程式

漫談c++11 Thread庫之使寫多執行緒程式

      c++11中最重要的特性之一就是對多執行緒的支援了,然而《c++ primer》5th卻沒有這部分內容的介紹,著實人有點遺憾。在網上了解到了一些關於thread庫的內容。這是幾個比較不錯的學習thread庫的資源:

前兩個網站我還是非常喜歡的,都是線上的幫助手冊,兩個選擇其中一個就可以了,看你口味選擇就好了。最後一個是原版的《C++ Concurrency In Action》,非常棒的一本書,鑑於中文譯版已經被黑出翔了,所以能看英文就看英文版吧,我也是硬著頭皮啃了一點兒。以下是我學習thread庫的一點感受和見解,如果你有發現我的錯誤,還請你及時批評指正,我將萬分感謝

      有關執行緒、併發相關的基礎知識,我就不浪費篇幅了。

一個簡單的使用執行緒的Demo

       c++11提供了一個新的標頭檔案<thread>提供了對執行緒函式的支援的宣告(其他資料保護相關的宣告放在其他的標頭檔案中,暫時先從thread標頭檔案入手吧),寫一個多執行緒的程式需要引用這個新的標頭檔案:

#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就是一個簡單的多執行緒的應用了。其輸出如下:

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的實現版本

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()的用法一樣,具體區別建議還是看看標頭檔案比較好,這裡我就多個事,順手就貼出來了:

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:

#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;
}

得到了預期結果:

[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了。光靠文字還是略顯蒼白的,肯定還是程式碼更加直觀:

#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';
}

執行結果:

[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上實現如下:

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; }
      
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:

#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位的整數,具體取決於實現,當然具體怎麼實現並無區別,我們只要關心它可以作為標識執行緒的一種手段:

[thread]main
140302328772352
140302328772352

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

      關於何時用到std::thread::id::operator<,我暫時沒有搞清楚,如果您知道,不妨告訴我,我將萬分感激。

理解了以上內容,我們基本可以使用多執行緒去實現一些簡單的任務了,當然要想安全地使用執行緒,這還是遠遠不夠的。接下來我還要再探、三探thread庫。

      如若以上博文有錯誤、誤導之處,請你原諒,還望批評指正,我在此先謝過各位。