一. 線程的等待與分離ios
(一)join和detach函數程序員
1. 線程等待:join()編程
(1)等待子線程結束,調用線程處於阻塞模式。緩存
(2)join()執行完成以後,底層線程id被設置爲0,即joinable()變爲false。同時會清理線程相關的存儲部分, 這樣 std::thread 對象將再也不與已經底層線程有任何關聯。這意味着,只能對一個線程使用一次join();調用join()後,joinable()返回false。安全
2. 線程分離:detach()數據結構
(1)分離子線程,與當前線程的鏈接被斷開,子線程成爲後臺線程,被C++運行時庫接管。這意味着不可能再有std::thread對象能引用到子線程了。與join同樣,detach也只能調用一次,當detach之後其joinable()爲false。函數
(2)注意事項:oop
①若是不等待線程,就必須保證線程結束以前,可訪問的數據是有效的。特別是要注意線程函數是否還持有一些局部變量的指針或引用。性能
②爲防止上述的懸空指針和懸引用的問題,線程對象的生命期應儘可能長於底層線程的生命期。測試
(3)應用場合
①適合長時間運行的任務,如後臺監視文件系統、對緩存進行清理、對數據結構進行優化等。
②線程被用於「發送即無論」(fire and forget)的任務,任務完成狀況線程並不關心,即安排好任務以後就無論。
(二)聯結狀態:一個std::thread對象只可能處於可聯結或不可聯結兩種狀態之一。可用joinable()函數來判斷,即std::thread對象是否與某個有效的底層線程關聯(內部經過判斷線程id是否爲0來實現)。
1. 可聯結(joinable):當線程可運行、己運行或處於阻塞時是可聯結的。注意,若是某個底層線程已經執行完任務,可是沒有被join的話,該線程依然會被認爲是一個活動的執行線程,仍然處於joinable狀態。
2. 不可聯結(unjoinable):
(1)當不帶參構造的std::thread對象爲不可聯結,由於底層線程還沒建立。
(2)己移動的std::thread對象爲不可聯結。由於該對象的底層線程id會被設置爲0。
(3)己調用join或detach的對象爲不可聯結狀態。由於調用join()之後,底層線程己結束,而detach()會把std::thread對象和對應的底層線程之間的鏈接斷開。
【編程對象】等待與分離
#include <iostream> #include <thread> using namespace std; //1. 懸空引用問題 class FuncObject { void do_something(int& i) { cout <<"do something: " << i << endl; } public: int& i; FuncObject(int& i) :i(i) { } void operator()() { for (unsigned int j = 0; j < 1000; ++j) { do_something(i); //可能出現懸空引用的問題。 } } }; void oops() { int localVar = 0; FuncObject fObj(localVar); std::thread t1(fObj); t1.detach(); //子線程分離,轉爲後臺運行。主線程調用oops函數,可能出現oops函數 //執行完了,子線程還在運行的現象。它會去調用do_something,這時會 //訪問到己經被釋放的localVar變量,會出現未定義行爲!若是這裏改爲 //join()則不會發生這種現象。所以主線程會等子線程執行完才退出oops } //2. 利用分離線程處理多文檔文件 void openDocAndDisplay(const std::string& fileName){} //打開文件 bool doneEditing() { return false; } //判斷是否結束編輯 enum class UserCommand{OpenNewDocument, SaveDocument,EditDocument}; //命令類型 UserCommand getUserInput() { return UserCommand::EditDocument; } //獲取用戶命令 string getFilenameFromUser() { return ""; } //獲取文件名 void processUserInput(UserCommand cmd){} //處理其它命令 void editDocument(const std::string& fileName) { openDocAndDisplay(fileName); while (!doneEditing()) { UserCommand cmd = getUserInput(); if (cmd == UserCommand::OpenNewDocument) { //若是用戶選擇打開一個新文檔 const string newName = getFilenameFromUser(); std::thread t(editDocument, newName); //啓動新線程去處理這個新文檔 t.detach(); //子線程分離。這樣主線程就能夠繼續處理其餘任務。 }else { processUserInput(cmd); } } } int main() { //1. 懸空引用問題 oops(); //2. 利用分離線程處理多文檔文件 editDocument("E:\\Demo\\abc.doc"); return 0; }
二. std::thread對象的析構
(一)std::thread的析構
1. std::thread對象析構時,會先判斷joinable(),若是可聯結,則程序會直接被終止(terminate)。
2. 這意味std::thread對象從其它定義域出去的任何路徑,都應爲不可聯結狀態。也意味着建立thread對象之後,要在隨後的某個地方顯式地調用join或detach以便讓std::thread處於不可聯結狀態。
(二)爲何析構函數中不隱式調用join或detach?
1. 若是設計成隱式join():將致使調用線程一直等到子線程結束才返回。若是子線程正在運行一個耗時任務,這可能形成性能低下的問題,並且問題也不容易被發現。
2. 若是設計成隱式detach():因爲detach會將切斷std::thread對象與底層線程之間的關聯,兩個線程今後各自獨立運行。若是線程函數是按引用(或指針)方式捕捉的變量,在調用線程退出做用域後這些變量會變爲無效,這容易掩蓋錯誤也將使調試更加困難。所以隱式detach,還不如join或者顯式調用detach更直觀和安全。
3.標準委員會認爲,銷燬一個joinable線程的後果是十分可怕的,所以他們經過terminate程序來禁止這種行爲。爲了不銷燬一個joinable的線程,就得由程序員本身來確保std::thread對象從其定義的做用域出去的任何路徑,都處於不可聯結狀態,最經常使用的方法就是資源獲取即初始化技術(RAII,Resource Acquisition Is Initialization)。
(三)std::thread對象與RAII技術的結合
1. 方案1:自定義的thread_guard類,並將std::thread對象傳入其中,同時在構造時選擇join或detach策略。當thread_guard對象析構時,會根據析構策略,調用std::thread的join()或detach(),確保在任何路徑,線程對象都處於unjoinable狀態。
2. 方案2:從新封裝std::thread類(見下面的代碼,類名爲joining_thread),在析構時隱式調用join()。
【編程實驗】利用RAII確保std::thread全部路徑皆爲unjoinable
#include <iostream> #include <thread> #include <functional> #include <algorithm> using namespace std; constexpr auto tenMillion = 10000000; bool conditionsAreSatisfied() { return false;}//return true or false //問題函數:doWork_oops(沒有確保std::thread全部皆爲不可聯結) //參數:filter過濾器,選0至maxVal之間的值選擇出來並放入vector中 bool doWork_oops(std::function<bool(int)> filter, int maxVal = tenMillion) { std::vector<int> goodVals; //保存通過濾器篩選出來的數值(0-maxVal) std::thread t([&filter, maxVal, &goodVals] { //注意goodVals是局部變量,按引用傳入子線程。 for (auto i = 0; i <= maxVal; ++i) if (filter(i)) goodVals.push_back(i); }); if (conditionsAreSatisfied()) { //若是一切就緒,就開始計算任務 t.join(); //等待子線程結束 //performComputation(goodVals); //主線程執行計算任務 return true; } //conditionsAreSatisfied()時false,表示條件不知足。(注意,仍沒調用join()或detach()) return false; //調用線程(通常是主線程)執行到這裏,t對象被析構,std::thread的析構函數被調用, //此時因爲子線程仍處於可聯結狀態,將執行std::ternimate終止程序! //爲何std::thread析構函數不隱式執行join或detach,而是終止程序的運行? //若是隱式調用join()會讓主線程等待子線程(耗時任務)結束,這會浪費性能。 //而若是隱式調用detach會使主線程和子線程分離,子線程因爲引用goodVals局部變量, //會出現懸空引用的問題,但這問題又不容易被發現。所以,經過std::ternimate來終止 //程序,以便讓程序員本身決定和消除這些問題。好比繼續調用join(),仍是detach(但需 //要同時解決懸空引用問題)? } //利用RAII技術,確保std::thread的正常析構 class thread_guard //scoped_thread { public: enum class DtorAction{join, detach}; //析構行爲 //構造函數只接受右值類型,由於std::thread只能被移動。雖然t爲右值引用類型,但因爲形參自己 //左值,所以調用std::move將形參轉爲右值。 thread_guard(std::thread&& t, DtorAction a = DtorAction::join):action(a), thr(std::move(t)) { } ~thread_guard() { if (thr.joinable()) //必須校驗,join和detach只能被調用一次 { if (action == DtorAction::join) { thr.join(); } else { thr.detach(); } } } std::thread& get() { return thr; } //因爲聲明瞭析構函數,編譯器將再也不提供移動操做函數,所以需手動生成 thread_guard(thread_guard&&) noexcept = default; thread_guard& operator=(thread_guard&&) = default; //本類不支持複製 thread_guard(const thread_guard&) = delete; thread_guard& operator=(const thread_guard&) = delete; private: //注意action和thr的聲明順序,因爲thr被建立之後會執行起來,必須 //保證action己被初始化。所以先聲明action,再聲明thr。 DtorAction action; std::thread thr; }; bool doWork_ok(std::function<bool(int)> filter, int maxVal = tenMillion) { std::vector<int> goodVals; std::thread t([&filter, maxVal, &goodVals] { //注意goodVals是局部變量,按引用傳入子線程。 for (auto i = 0; i <= maxVal; ++i) if (filter(i)) { cout << i << endl; goodVals.push_back(i); } }); thread_guard guard(std::move(t));//默認析構策略是thread_guard::DtorAction::join if (conditionsAreSatisfied()) { //若是一切就緒,就開始計算任務 guard.get().join(); //等待子線程結束 //performComputation(goodVals); //主線程執行計算任務 return true; } //conditionsAreSatisfied()時false,表示條件不知足。guard對象析構,但會隱式調std::thread對象 //的join()。 return false; } //使用RAII等待線程完成:joining_thread類的實現 class joining_thread { std::thread thr; public: joining_thread() noexcept = default; //析構函數 ~joining_thread() { if (joinable()) //對象析構造,會隱式調用join() { join(); } } template<typename Callable, typename... Args> explicit joining_thread(Callable&& func, Args&& ...args): thr(std::forward<Callable>(func), std::forward<Args>(args)...) { } //類型轉換構造函數 explicit joining_thread(std::thread t) noexcept : thr(std::move(t)) { } //移動操做 joining_thread(joining_thread&& other) noexcept : thr(std::move(other.thr)) { } joining_thread& operator=(joining_thread&& other) noexcept { if (joinable()) join(); //等待原線程執行完 thr = std::move(other.thr); //將新線程移動到thr中 return *this; } joining_thread& operator=(std::thread other) noexcept { if (joinable()) join(); thr = std::move(other); return *this; } bool joinable() const noexcept { return thr.joinable(); } void join() { thr.join(); } void detach() { thr.detach(); } void swap(joining_thread& other) noexcept { thr.swap(other.thr); } std::thread::id get_id() const noexcept { return thr.get_id(); } std::thread& asThread() noexcept //轉化爲std::thread對象 { return thr; } const std::thread& asThread() const noexcept { return thr; } }; void doWork(int i) { cout << i << endl; } int main() { //1.問題函數:doWork_oops:沒有確保std::thread的全部路徑都爲joinable //doWork_oops([](auto val) { return val >= 100; }, 1000); //2. doWork_ok函數 doWork_ok([](auto val) { return val >= 100; }, 1000); //3. 測試joining_thread類 std::vector<joining_thread> threads; //joining_thread析構時隱式調用join for (unsigned int i = 0; i < 20; ++i) { threads.push_back(joining_thread(doWork, i)); } std::for_each(threads.begin(), threads.end(), std::mem_fn(&joining_thread::join)); return 0; }