一. std::thread類ios
(一)thread類摘要及分析編程
class thread { // class for observing and managing threads public: class id; using native_handle_type = void*; thread() noexcept : _Thr{} { // 建立空的thread對象,實際上線程並未被建立! } private: template <class _Tuple, size_t... _Indices> static unsigned int __stdcall _Invoke(void* _RawVals) noexcept { // enforces termination //接口適配:將用戶的可調用對象與_beginthreadex的接口進行適配。 //子線程從新擁有從主線程轉讓過來的保存着thread參數副本的tuple堆對象的全部權。 const unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals)); _Tuple& _Tup = *_FnVals; _STD invoke(_STD move(_STD get<_Indices>(_Tup))...); //注意,因爲tuple中保存的都是副本,所以全部的參數都以右值的方式被轉發出去。 _Cnd_do_broadcast_at_thread_exit(); // TRANSITION, ABI return 0; } template <class _Tuple, size_t... _Indices> _NODISCARD static constexpr auto _Get_invoke( index_sequence<_Indices...>) noexcept { // select specialization of _Invoke to use return &_Invoke<_Tuple, _Indices...>; //這裏返回特化的_Invoke函數指針 } public: template <class _Fn, class... _Args, class = enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>>> explicit thread(_Fn&& _Fx, _Args&& ... _Ax) { // construct with _Fx(_Ax...) using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>; //將傳入thread的全部參數保存着tuple //在堆上建立tuple以按值保存thread全部參數的副本,指針用unique_ptr來管理。 auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...); //建立tuple的智能指針 constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{}); //獲取線程函數地址 //在Windows系統中,會調用_beginthredex來建立新線程。其中,_Invoker_proc爲線程函數地址,它要求的參數爲tuple的指針,即_Decay_copied.get() //注意:線程建立後即當即運行(第5個參數爲0),原生的線程id保存在_Thr._Id中,句柄保存在_Thr._Hnd。 _Thr._Hnd = reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id)); if (_Thr._Hnd == nullptr) { // failed to start thread _Thr._Id = 0; _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN); } else { // ownership transferred to the thread (void)_Decay_copied.release(); //轉讓tuple的全部權給新的線程。 } } ~thread() noexcept { // clean up if (joinable()) { //注意,std::thread析構時,若是線程仍可joinable,則會調用terminate終止程序! _STD terminate(); } } thread(thread&& _Other) noexcept : _Thr(_STD exchange(_Other._Thr, {})) { // move from _Other } thread& operator=(thread&& _Other) noexcept { // move from _Other if (joinable()) { _STD terminate(); } _Thr = _STD exchange(_Other._Thr, {}); return *this; } thread(const thread&) = delete; //thread對象不能被複制 thread& operator=(const thread&) = delete; //thread對象不能被拷貝賦值 void swap(thread& _Other) noexcept { // swap with _Other _STD swap(_Thr, _Other._Thr); } _NODISCARD bool joinable() const noexcept { // return true if this thread can be joined return _Thr._Id != 0; //原生的線程id不爲0,表示底層的線程己經建立 } void join() { // join thread if (!joinable()) { _Throw_Cpp_error(_INVALID_ARGUMENT); } if (_Thr._Id == _Thrd_id()) { _Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR); } if (_Thrd_join(_Thr, nullptr) != _Thrd_success) { _Throw_Cpp_error(_NO_SUCH_PROCESS); } _Thr = {}; //注意調用join之後,原生線程id被清零,意味着join只能被調用一次! } void detach() { // detach thread if (!joinable()) { _Throw_Cpp_error(_INVALID_ARGUMENT); } _Check_C_return(_Thrd_detach(_Thr)); //線程被分離,成爲後臺線程 _Thr = {}; //注意調用detach之後,原生線程id被清零,意味着detach也只能被調用一次! } _NODISCARD id get_id() const noexcept; _NODISCARD static unsigned int hardware_concurrency() noexcept { // return number of hardware thread contexts return _Thrd_hardware_concurrency(); } _NODISCARD native_handle_type native_handle() { // return Win32 HANDLE as void * return _Thr._Hnd; } private: _Thrd_t _Thr; };
1. 構造std::thread對象時,若是不帶參則會建立一個空的thread對象,但底層線程並無真正被建立。若是帶參則會建立新線程,並且會被當即運行。通常用於接受其餘std::thread對象,經過move移入其中。ide
2. 在建立thread對象時,std::thread構建函數中的全部參數均會按值並以副本的形式保存成一個tuple對象。該tuple由調用線程(通常是主線程)在堆上建立,並交由子線程管理,在子線程結束時同時被釋放。函數
3. joinable():用於判斷std::thread對象聯結狀態,一個std::thread對象只可能處於可聯結或不可聯結兩種狀態之一。this
(1)可聯結:當線程己運行或可運行、或處於阻塞時是可聯結的。注意,若是某個底層線程已經執行完任務,可是沒有被join的話,仍然處於joinable狀態。即std::thread對象與底層線程保持着關聯時,爲joinable狀態。spa
(2)不可聯結:操作系統
①當不帶參構造的std::thread對象爲不可聯結,由於底層線程還沒建立。線程
②己移動的std::thread對象爲不可聯結。3d
③己調用join或detach的對象爲不可聯結狀態。由於調用join()之後,底層線程己結束,而detach()會把std::thread對象和對應的底層線程之間的鏈接斷開。指針
4. std::thread對象析構時,會先判斷是否可joinable(),若是可聯結,則會程序會直接被終止。這意味着建立thread對象之後,要在隨後的某個地方調用join或detach以便讓std::thread處於不可聯結狀態。
5. std::thread對象不能被複制和賦值,只能被移動。
(二)線程的基本用法
1. 獲取當前信息
(1)線程ID:t.get_id(); //其中t爲std::thread對象。
(2)線程句柄:t.native_handle() //返回與操做系統相關的線程句柄。
(3)獲取CPU核數:std::thread::hardware_concurrency(),失敗時返回0。
2.線程等待和分離
(1)join():等待子線程,調用線程處於阻塞模式
(2)detach():分離子線程,與當前線程的鏈接被斷開,子線程成爲後臺線程,被C++運行時庫接管。
(3)joinable():檢查線程是否可被聯結。
(三)std::this_thread命名空間中相關輔助函數
1. get_id(); //獲取線程ID:
2. yield(); //當前線程放棄執行,操做系統轉去調度另外一線程。
3. sleep_until(const xtime* _Abs_time):線程休眠至某個指定的時刻(time point),該線程才被從新喚醒。
4. sleep_for(std::chrono::seconds(3));//睡眠3秒後才被從新喚醒,不過因爲線程調度等緣由,實際休眠時間可能比 sleep_duration 所表示的時間片更長。
【編程實驗】std::thread的基本用法
#include <iostream> #include <thread> #include <chrono> //for std::chrono::seconds #include <ctime> //for std::time_t #include <iomanip> //for std::put_time using namespace std; using namespace std::chrono; void thread_func(int x) { cout <<"thread_func start..." << endl; cout << "x = " << x << endl; cout << "child thread id: " << std::this_thread::get_id() << endl; std::this_thread::yield(); //當前線程放棄執行 cout <<"thread_func end." << endl; } void test_sleepUntil() { std::cout <<"thread id " << std::this_thread::get_id() << "'s sleepUntil begin..." << endl; using std::chrono::system_clock; std::time_t tStart = system_clock::to_time_t(system_clock::now()); //to_time_t:將time_point轉爲std::time_t struct std::tm tm; localtime_s(&tm,&tStart); std::cout << "Current time: " << std::put_time(&tm, "%X") << std::endl; //X須大寫,若小寫輸出日期 std::cout << "Waiting for the next minute..." << std::endl; ++tm.tm_min; tm.tm_sec = 0; std::this_thread::sleep_until(system_clock::from_time_t(mktime(&tm))); //from_time_t:將time_t轉爲time_point std::cout << std::put_time(&tm, "%X") <<" reach."<< std::endl; std::cout << "thread id " << std::this_thread::get_id() << "'s sleepUntil end." << endl; } int main() { //1. 獲取當前線程信息 cout << "hardware_concurrency: " << std::thread::hardware_concurrency() << endl; //8,當前cpu核數 cout << "main thread id: " <<std::this_thread::get_id() << endl; //當前線程(主線程)id std::thread t(thread_func, 5); cout <<"child thread id: " <<t.get_id() << endl; //子線程id cout << "child thread handle: " << t.native_handle() << endl; //2.joinable檢查 cout << endl; std::this_thread::sleep_for(std::chrono::seconds(3)); //主線程睡眠3秒,等待子線程結束 if (t.joinable()) cout << "t is joinable" << endl; //該行打印,說明子線程己結束時,仍處於joinable狀態!!! else cout << "t is unjoinable" << endl; t.join(); //sleep_until cout << endl; std::thread t2(test_sleepUntil); t2.join(); //傳入lambda cout << endl; std::thread t3([]() {cout <<"t3(thread id: " << std::this_thread::get_id()<< ") is running..." << endl; }); t3.join(); return 0; } /*輸出結果 hardware_concurrency: 8 main thread id: 17672 child thread id: 8172 child thread handle: 000000E4 thread_func start... x = 5 child thread id: 8172 thread_func end. t is joinable thread id 8016's sleepUntil begin... Current time: 23:21:25 Waiting for the next minute... 23:22:00 reach. thread id 8016's sleepUntil end. t3(thread id: 2880) is running... */
二. 傳遞參數的方式
(一)傳參中的陷阱:
1. 向std::thread 構造函數傳參:全部參數(含第1個參數可調用對象)均按值並以副本的形式保存在std::thread對象中的tuple裏。這一點的實現相似於std::bind。若是要達到按引用傳參的效果,可以使用std::ref來傳遞。
2. 向線程函數的傳參:因爲std::thread對象裏保存的是參數的副本,爲了效率同時兼顧一些只移動類型的對象,全部的副本均被std::move到線程函數,即以右值的形式傳入。
(二)注意事項
1. 一個實參從主線程傳遞到子線程的線程函數中,須要通過兩次傳遞。第1次發生在std::thread構造時,這次參數按值並以副本形式被保存。第2次發生在向線程函數傳遞時,這次傳遞是由子線程發起,並將以前std::thread內部保存的副本以右值的形式(std::move())傳入線程函數中的。
2. 若是線程函數的形參爲T、const T&或T&&類型時,std::thread的構造函數能夠接受左值或右值實參。由於不論是左值仍是右值,在std::thread中均是以副本形式被保存,並在第2次向線程函數傳參時以右值方式傳入,而以上三種形參都可接受右值。
3. 而若是線程函數的形參爲T&時,不論是左值仍是右值的T類型實參,都是沒法直接經std::thread傳遞給形參爲T&的線程函數,由於該實參數的副本會被std::move成右值並傳遞線程函數,但T&沒法接受右值類型。所以,須要以std::ref形式傳入(具體原理見下面《編程實驗》中的註釋)。
4. 當向線程函數傳參時,可能發生隱式類型轉換,這種轉換是在子線程中進行的。須要注意,因爲隱式轉換會構造臨時對象,並將該對象(是個右值)傳入線程函數,所以線程函數的形參應該是可接受右值類型的T、const T&或T&&類型,但不能是T&類型。此外,若是源類型是指針或引用類型時,還要防止可能發生懸空指針和懸空引用的現象。
【編程實驗】std::thread傳參中的陷阱
#include <iostream> #include <thread> #include <chrono> using namespace std; using namespace std::chrono; //for std::chrono::seconds class Widget { public: mutable int mutableInt = 0; //Widget() :mutableInt(0) {} Widget() : mutableInt(0) { cout << "Widget(), thread id = "<< std::this_thread::get_id() << endl;} //類型轉換構造函數 Widget(int i):mutableInt(i){ cout << "Widget(int i), thread id = " << std::this_thread::get_id() << endl; } Widget(const Widget& w):mutableInt(w.mutableInt) { cout << "Widget(const Widget& w), thread id = " << std::this_thread::get_id() << endl; } Widget(Widget&& w) noexcept //移動構造 { mutableInt = w.mutableInt; cout << "Widget(Widget && w), thread id = " << std::this_thread::get_id() << endl; } void func(const string& s) { cout <<"void func(string& s), thread id = " << std::this_thread::get_id() << endl; } }; void updateWidget_implicit(const Widget& w) { cout << "invoke updateWidget_implicit, thread id =" << std::this_thread::get_id() << endl; } void updateWidget_ref(Widget& w) { cout << "invoke updateWidget_ref, thread id =" << std::this_thread::get_id() << endl; } void updateWidget_cref(const Widget& w) { cout << "invoke updateWidget_cref, thread id =" << std::this_thread::get_id() << endl; } void test_ctor(const Widget& w) //注意這裏的w是按引用方式傳入(引用的是std::thread中保存的參數副本) { cout << "thread begin...(id = " << std::this_thread::get_id() << ")" << endl; cout << "w.matableInt = " << ++w.mutableInt << endl;//注意,當std::thread按值傳參時,此處修改的是std::thread中 //保存的參數副本,而不是main中的w。 //而當向std::thread按std::ref傳參時,先會建立一個std::ref臨時對象, //其中保存着main中w引用。而後這個std::ref再以副本的形式保存在 //std::thread中。隨後這個副本被move到線程函數,因爲std::ref重載了 //operator T&(),所以會隱式轉換爲Widget&類型(main中的w),所以起到 //的效果就好象main中的w直接被按引用傳遞到線程函數中來。 cout << "thread end.(id = " << std::this_thread::get_id() << ")" << endl; } int main() { //1. 向std::thread構造函數傳參 cout << "main thread begin...(id = "<<std::this_thread::get_id()<<")"<< endl; Widget w; cout << "-----------test std::thread constructor----------------------- "<< endl; //1.1 std::thread默認的按值傳參方式:全部的實參都是被拷貝到std::thread對象的tuple中,即以副本形式被保存起來。 std::thread t1(test_ctor, w); //注意,w是按值保存到std::thread中的,會調用其拷貝構造函數。 t1.join(); cout << "w.mutableInt = " << w.mutableInt << endl; //0,外部的w沒受影響。mutableInf仍爲0。 cout << endl; //1.2 std::thread按引用傳參(std::ref) std::thread t2(test_ctor, std::ref(w)); //注意,w是按引用傳入到std::thread中的,不會調用其拷貝構造函數。 t2.join(); cout << "w.mutableInt = " << w.mutableInt << endl; //1,因爲w按引用傳遞,mutableInf被修改成1。 cout << "------------------test thread function------------------------ " << endl; //2. 向線程函數傳遞參數 //2.1 線程函數的參數爲引用時 //2.1.1 線程函數形參爲T& //std::thread t3(updateWidget_ref, w); //編譯失敗,由於std::thread內部是以右值形式向線程函數updateWidget_ref(Widget&)傳 //參的,而右值沒法用來初始化Widget&引用。 std::thread t3(updateWidget_ref, std::ref(w)); //ok,緣由相似test_ctor函數中的分析。即當線程函數的形參爲T&時, //通常以std::ref形式傳入 t3.join(); //2.1.2 線程函數形參爲const T& std::thread t4(updateWidget_cref, w); //ok,但要注意w會先被拷貝構造一次,以副本形式保存在thread中。該副本再被以右值 //形式傳遞給線程函數updateWidget_cref(const Widget&),而const T&可接受右值。 t4.join(); //2.2 隱式類型轉換及臨時對象 const char* name = "Santa Claus"; //注意: //(1)當向std::thread傳入類成員函數時,必須用&才能轉換爲函數指針類型 //(2)類成員函數的第1個參數是隱含的this指針,這裏傳入&w。 //(3)本例會發生隱式類型轉換,首先name在主線程中以const char*類型做爲副本被保存在thread中,當向線程函數 // Widget::func(const string&)傳參時,會先將以前的name副本隱式轉換爲string臨時對象再傳入,所以線程函數的形參中 // 須要加const修飾。同時要注意,這個隱式轉換髮生在子線程調用時,即在子線程中建立這個臨時對象。這就須要確保主線 // 程的生命週期長於子線程,不然name副本就會變成野指針,從而沒法正確構造出string對象。 std::thread t5(&Widget::func, &w, name); //ok。 t5.join(); //若是這裏改爲t5.detach,而且若是主線程生命期在這行結束時,就可能發生野指針現象。 std::thread t6(&Widget::func, &w, string(name)); //爲了不上述的隱式轉換能夠帶來的bug。能夠在主線程先構造好這個 //string臨時對象,再傳入thread中。(如左) t6.join(); //如下證實隱式轉換髮生在子線程中 cout << endl; std::thread t7(updateWidget_implicit, 1); //會將1隱式轉換爲Widget,這個隱式轉換髮生在子線程。由於1會先以int型的副本 //保存在t7中,當向線程函數傳參時,纔將int經過Widget的類型轉換構造轉成Widget。 t7.join(); cout << "main thread end.(id = " << std::this_thread::get_id() << ")" << endl; return 0; } /*輸出結果: main thread begin...(id = 8944) Widget(), thread id = 8944 -----------test std::thread constructor----------------------- Widget(const Widget& w), thread id = 8944 //w被按值保存std::thread中。會調用拷貝構造函數 thread begin...(id = 17328) w.matableInt = 1 //只是修改std::thread中w副本的值。 thread end.(id = 17328) w.mutableInt = 0 //main中的w沒被修改 thread begin...(id = 5476) w.matableInt = 1 //按std::ref傳遞既修改std::thread中w副本的值,也修改了main中w的值。 thread end.(id = 5476) w.mutableInt = 1 ------------------test thread function------------------------ invoke updateWidget_ref, thread id =17828 Widget(const Widget& w), thread id = 8944 invoke updateWidget_cref, thread id =2552 void func(string& s), thread id = 11332 void func(string& s), thread id = 17504 Widget(int i), thread id = 8996 //隱式轉換髮生在子線程8996中 invoke updateWidget_implicit, thread id =8996 main thread end.(id = 8944) */