C++ thread 基礎
1. 基本的 thread 建立
標準庫的 thread 非常容易存取
#include <thread> #include <iostream> using std::cout; void hello() { cout << "hello" << '\n'; } int main() { std::thread t(hello); t.join(); }
- 引入=thread=標頭檔
- 宣告函式
- 建構一個=thread=物件
- 用=join=讓=main=等待它完成
2. 傳入參數
很好,程式會運作,可是我們想要知道如何對 thread 函數傳入參數
void hello(int i) { cout << "hello, " << i << '\n'; }
這裡就需要更動函數的宣告式,但呼叫處不能直接寫下直覺的
std::thread t(hello(2));
因為這不是傳入函數,而是傳函數的結果給 thread,那不是我們需要的東西。 而正確的寫法是
std::thread t(hello, 2);
我們可以從這個 Mingw 版本的實作中看到參數怎麼傳進去的
template<class Function, class... Args> explicit thread(Function&& f, Args&&... args) { typedef decltype(std::bind(f, args...)) Call; Call* call = new Call(std::bind(f, args...)); mHandle = (HANDLE)_beginthreadex(NULL, 0, threadfunc<Call>, (LPVOID)call, 0, (unsigned*)&(mThreadId.mId)); if (mHandle == _STD_THREAD_INVALID_HANDLE) { int errnum = errno; delete call; throw std::system_error(errnum, std::generic_category()); } }
事實上,我們不只能傳入函數給 Thread,我們可以傳任何可呼叫(callable)物件進去。 用法非常簡單,就是定義一個具有 operator()的 class,然後用這個 class 產生物件。
class Ya { public: void operator()() const { cout << "Ya" << '\n'; } };
就像這樣
std::thread t( Ya() );
我們用原本的寫法,卻發現編譯失敗,原來是因為這個寫法被編譯器當作函式宣告,而不是一個物件定義。
2.1. 第一種作法:加上括號
std::thread t( (Ya()) );
2.2. 第二種作法:用大括號初始運算子
std::thread t{ Ya() };
第二種作法比較好,用大括號是官方推薦的寫法,而且不會有被誤判的問題。 第一種作法則讓人難以理解為什麼加個括號就能解決問題。
2.3. 第三種作法:用 lambda
再介紹一種作法
std::thread t3([] { cout << "lambda" << '\n'; });
利用=lambda=運算式,不過就算是用 lambda
語法也應該用大括號運算子建立
thread。 畢竟,通常沒有理由不用專門用來初始化的大括號。
3. join
那麼=join=呢? =thread=物件一旦建立,啟動執行緒,你就要明確的決定要
- 等待執行緒結束(
join
) - 讓它自己旁邊玩沙(
deatch
)
如果沒有在 thread 物件被清除之前決定,那程式就會終止。 因為下面的程式中,
~thread() { if (joinable()) std::terminate(); }
解構子會呼叫 std::terminate()
讓整隻程式掛掉(除非去改變可連結狀態)。
bool joinable() const {return mHandle != _STD_THREAD_INVALID_HANDLE;}
這是=joinable=的實作,因為名稱取的很好,所以可以看出只要 thread
狀態沒有被合法的處理(上面兩個狀況,=join=與=detach=)時會回傳=true=,
在適當的時候引發=terminate=。所以即使發生例外,也要確保執行緒成功被設定好要怎麼處理。
從這裡應該很容易看出來,=thread= 物件可不是 thread
本身,而是 thread
的持有者,千萬不要搞混它們的意義。
4. crash
要讓程式掛掉真的很容易
std::thread t( hello, i );
不決定的結果就是程式=panic=
5. 例外處理
沒有程式遇不到例外,執行緒程式也不例外。
前面我們提到,如果沒有決定如何處理 thread
物件,程式就會異常退出。
問題是,遇到例外時怎麼辦?
5.1. 兩個情況分別 join
第一種辦法很土,不過反正能解決問題就是了
std::thread t(hello); try { // ... } catch (int err) { t.join(); } t.join();
就是寫兩次,這個方法實際上會帶來很大的問題,因為我們很可能會忘記寫其中一個
join=。 最後除錯追蹤看到 =terminate
然後還想說
我為什麼會呼叫 terminate?
因為你沒有直接呼叫,最後憤怒的挖到 thread 函式庫裡面去。
5.2. 用 RAII 保證
更好的辦法是用 RAII 保證
class Thread_guard { std::thread t; public: explicit Thread_guard(std::thread& t_) : t{t_} {} ~Thread_guard() { if (t.joinable()) { t.join(); } } }
現在我們只要把 thread
放進去就好了,值得一提的是,這種物件最好移除複製建構子和複製指派運算子。
Thread_guard(Thread_guard const&) = delete; Thread_guard& operator=(Thread_guard const&) = delete;
兩種操作對這個物件而言都異常危險,我們將無法預測會發生什麼事。 宣告為 delete 之後,試圖做上述操作都會直接被編譯器擋下。 用法非常明確
std::thread t{func} Thread_guard tg{t} // do something ...
這樣一來,只要離開資源,物件的解構式被啟動,就會決定怎麼處理 thread 物件。 最後,注意=cout=其實不能那樣用,你可以試試使用迴圈讓執行緒印更多東西,然後你會發現文字會不按順序的亂印。 這是正常的,因為它們交錯的使用=cout=,而沒有一個資源管理的方式。 最簡單的方式就是上鎖,當然也有對這類行為不太介意的程式,例如共享的資源是唯讀的。