c++11中最重要的特性之一就是對多線程的支持了,然而《c++ primer》5th卻沒有這部分內容的介紹,著實人有點遺憾。在網上了解到了一些關於thread庫的內容。這是幾個比較不錯的學習thread庫的資源:
Thread support library : http://en.cppreference.com/w/cpp/thread
Cpp reference/thread : http://www.cplusplus.com/reference/thread/
<< C++ Concurrency In Action >> :http://files.cnblogs.com/files/ittinybird/CplusplusConcurrencyInAction.pdf //Recommend!
前兩個網站我還是非常喜歡的,都是在線的幫助手冊,兩個選擇其中一個就可以了,看你口味選擇就好了。最後一個是原版的《C++ Concurrency In Action》,非常棒的一本書,鑒於中文譯版已經被黑出翔了,所以能看英文就看英文版吧,我也是硬著頭皮啃了一點兒。以下是我學習thread庫的一點感受和見解,如果你有發現我的錯誤,還請你及時批評指正,我將萬分感謝。
有關線程、並發相關的基礎知識,我就不浪費篇幅了。
一個簡單的使用線程的Democ++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!
因此我們可以猜測到它的執行流大致是這樣的:
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++運行庫保證,當線程退出時,其相關資源的能夠正確的回收。
分離的線程,大致上是這樣執行的,它運行結束後,不再需要通知調用它的線程:
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庫。
如若以上博文有錯誤、誤導之處,請你原諒,還望批評指正,我在此先謝過各位。