1. std::thread基本介紹
1)構造std::thread對象時,如果不帶參則會創建一個空的thread對象,但底層線程並沒有真正被創建,一般可將其他std::thread對象通過move移入其中;
如果帶參則會創建新線程,而且會被立即運行。
2)joinable():用於判斷std::thread對象聯結狀態,一個std::thread對象只可能處於可聯結或不可聯結兩種狀態之一。
a. 可聯結:當線程己運行或可運行、或處於阻塞時是可聯結的。注意,如果某個底層線程已經執行完任務,但是沒有被join的話,仍然處於joinable狀態。
即std::thread對象(對象由父線程所有)與底層線程保持着關聯時,為joinable狀態。
b. 不可聯結:
① 當不帶參構造的std::thread對象為不可聯結,因為底層線程還沒創建。
② 己移動的std::thread對象為不可聯結。
③ 己調用join或detach的對象為不可聯結狀態。因為調用join()以后,底層線程己結束,而detach()會把std::thread對象和對應的底層線程之間的連接斷開。
join():等待子線程,調用線程處於阻塞模式。join()執行完成之后,底層線程id被設置為0,即joinable()變為false。
detach():分離子線程,與當前線程的連接被斷開,子線程成為后台線程,被C++運行時庫接管。
3)std::thread對象析構時,會先判斷是否可joinable(),如果可聯結,則程序會直接被終止出錯。這意味着創建thread對象以后,要在隨后的某個地方調用join或
detach以便讓std::thread處於不可聯結狀態。
4)std::thread對象不能被復制和賦值,只能被移動。
5)獲取當前信息
// t為std::thread對象 t.get_id(); // 獲取線程ID t.native_handle(); // 返回與操作系統相關的線程句柄 std::thread::hardware_concurrency(); // 獲取CPU核數,失敗時返回0
6)std::this_thread命名空間中相關輔助函數
get_id(); // 獲取線程ID yield(); // 當前線程放棄執行,操作系統轉去調度另一線程 sleep_until(const xtime* _Abs_time); // 線程休眠至某個指定的時刻(time point),該線程才被重新喚醒 sleep_for(std::chrono::seconds(3)); // 睡眠3秒后才被重新喚醒,不過由於線程調度等原因,實際休眠時間可能比 sleep_duration 所表示的時間片更長
3. 傳遞參數的方式(2次傳參)
a. 第一次傳參(向 std::thread 構造函數傳參):在創建thread對象時,std::thread構建函數中的所有參數均會按值並以副本的形式保存成一個tuple對象。
該tuple由調用線程(一般是主線程)在堆上創建,並交由子線程管理,在子線程結束時同時被釋放。
注:如果要達到按引用傳參的效果,可使用std::ref來傳遞。
b. 第二次傳參(向線程函數的傳參):由於std::thread對象里保存的是參數的副本,為了效率同時兼顧一些只移動類型的對象,所有的副本均被
std::move到線程函數,即以右值的形式傳入,所以最終傳給線程函數參數的均為右值。
現在我們根據線程參數的類型展開討論:
首先先給出一個用作測試的類
class Test { public: mutable int mutableInt = 0; Test() : mutableInt(0) { cout << this << " " << "Test()" << endl; } Test(int i) : mutableInt(i) { cout << this << " " << "Test(int i)" << endl; } Test(const Test& w) : mutableInt(w.mutableInt) { cout << this << " " << "Test(const Test& w)" << endl; } Test(Test&& w) noexcept // 移動構造 { mutableInt = w.mutableInt; cout << this << " " << "Test(Test && w)" << endl; } void func(const string& s) { cout <<"void func(string& s)" << endl; } };
a. 線程參數為const T&類型:這里的引用是針對於第二次傳遞的參數,也就是直接引用保存在tuple中的參數副本,而不是最原始的參數,即
不是main中變量。但是你如果第一次向std::thread傳參時使用std::ref,首先會創建一個std::ref(它也是一個類)臨時對象,里面會保存着最原始
變量(main中的變量)的引用,然后這個std::ref臨時對象再以副本的形式保存在std::thread中,隨后這個副本被move到線程函數。由於std::ref
重載了類型轉換運算符operator T&(),因此會隱式轉換為Test&類型,因此起到的效果就好象main中的變量直接被按引用傳遞到線程函數中來。
現在按照這個理解來分析下面的代碼:
對於std::thread t1(test_ctor, w); 首先會調用一次拷貝構造,把w存儲為tuple元素;然后因為線程參數是引用,所以tuple元素給線程傳參數時不會發生
拷貝,但實際運行結果發現多輸出了一次拷貝,這應該是std::thread隱藏的實現細節,需要閱讀源碼了。
如果使用std::ref包裝的話,內部引用了原始的w,所以不會發生拷貝,但會發生std::ref對象的拷貝。
void test_ctor(const Test& w) { cout << &w << " " << "w.matableInt = " << ++w.mutableInt << endl; } int main() { Test w; // std::thread默認的按值傳參方式: 所有的實參都是被拷貝到std::thread對象的tuple中,即以副本形式被保存起來。 // 注意,w是按值保存到std::thread中的,會調用其拷貝構造函數。外部的w沒受影響。mutableInf仍為0。 std::thread t1(test_ctor, w); t1.join(); cout << "w.mutableInt = " << w.mutableInt << endl << endl; // std::thread按引用傳參(std::ref), 因為w是按引用傳入到std::ref對象中的,不會調用其拷貝構造函數。 // 由於w按引用傳遞,mutableInf被修改為1。 std::thread t2(test_ctor, std::ref(w)); t2.join(); cout << "w.mutableInt = " << w.mutableInt << endl; return 0; } // 第一部分輸出如下 Test(const Test& w) // 調用拷貝構造函數生成std::thread中的副本對象 Test(Test && w) // std::thread中的副本移動到線程參數 w.mutableInt = 0 // 第二部分 w.mutableInt = 1
b. 線程參數為T&類型:最終傳給線程函數參數的均為右值,而T&類型是不接受右值的,使用std::ref包裝后便不報錯了,因為它能隱式轉換成 T&。
void updateTest_ref(Test& w) { cout << &w << " " << "invoke updateTest_ref" << endl; } int main() { Test w; std::thread t1(updateTest_ref, w); // 編譯失敗,因為std::thread內部是以右值形式向線程函數updateTest_ref(Test&)傳參的, // 而右值無法用來初始化Test&引用。 std::thread t3(updateTest_ref, std::ref(w)); // ok, 原因類似test_ctor函數中的分析。即當線程函數的形參為T&時,一般以std::ref形式傳入 t3.join(); }