一. 線程的等待與分離
(一)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)注意事項:
①如果不等待線程,就必須保證線程結束之前,可訪問的數據是有效的。特別是要注意線程函數是否還持有一些局部變量的指針或引用。
②為防止上述的懸空指針和懸引用的問題,線程對象的生命期應盡量長於底層線程的生命期。
(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; }