C++11在標准庫中為多線程提供組件, 使用線程需要包含頭文件 thread, 其命名空間為 std.
每個進程至少有一個線程: 執行main()函數的線程, 其余線程有其各自的入口函數(線程函數)。
當線程執行完線程函數後, 線程也會退出. 如果不傳入線程函數(類似這種形式std::thread t;), 線程不會運行. 線程函數不能重載, 否則不能編譯.
在為一個線程創建了一個 std::thread 對象後, 如果線程已啟動(不傳入線程序函數時, 線程不會啟動), 必須要明確是加入(join)還是分離線程(detach).
// 啟動一個線程: void MyThread(const std::string& str) { PRINT_LINE_INFO(); std::cout << str << std::endl; } //std::thread t(MyThread, "Hello C..."); std::thread t([] { MyThread("Hello C..."); MyThread("Hello C2..."); }); // 對於類方法, 需要使用 std::bind. std::thread t(std::bind(&ThreadExample::MyThread, this, "msg")); ThreadGuard tg(t);
如果 std::thread 對象銷毀之前還沒有調用 join 或 detach, 程序就會終止( std::thread 的析構函數會調用 std::terminate() ). 因此, 即便是有異常存在, 也需要確保線程能夠正確的加入(joined)或分離(detached).
調用 join 或 detach 之前需要調用 joinable() 判斷一下線程是否運行. 如果 joinable() 返回 false, 則不需要.
join()是簡單粗暴的等待線程完成, 此時創建 std::thread 對象的線程(以下稱主線程)將被阻塞. 如果在線程啟動後到主線程在調用 join() 前的代碼中發生了異常, 此時將會導致主線程永遠沒有機會執行.
針對此問題, 需要使用 RAII 機制來解決, 如創建一個 ThreadGuard 對象, 在析構函數中保證總是可以調用到 join.
#ifndef _THREAD_GUARD_ #define _THREAD_GUARD_ #include <thread> class ThreadGuard { public: ThreadGuard(std::thread& t_) : t(t_){} ~ThreadGuard() { if (t.joinable()) { t.join(); } } ThreadGuard(const ThreadGuard &) = delete; ThreadGuard& operator=(const ThreadGuard &) = delete; private: std::thread& t; }; #endif // _THREAD_GUARD_
如果是分離線程, 必須保證可訪問數據的有效性, 否則會產生未定義的行為, 如同單線程中一個對象被銷毀後再訪問一樣.
處理這種情況的常規方法: 使線程函數的功能齊全, 將數據復制到線程中. 如果使用一個可調用的對象作為線程函數,這個對象就會復制到線程中,而後原始對象就可以銷毀. 下面是錯誤的使用方法示例:
class Func { int& i; public: Func(int& i_) : i(i_) {} void operator() () { for (unsigned j = 0; j < 10; ++j) { // 潛在訪問隱患:懸空引用 i std::cout << i << " "; } std::cout << std::endl; } }; { // 某個作用域內 int* p = new int(100); Func f(*p); std::thread t(f); t.detach(); // 不等待線程結束 delete p; } // 新線程可能還在運行
線程函數可以有不同的參數, 向線程傳遞參數,只要在構造 std::thread 對象時,按照線程函數參數列表一一對應傳入即可。線程函數有幾點需要注意的地方:
(1) 默認的參數會被拷貝到獨立的線程中,即使是引用的形式, 如果需要需要傳遞引用, 需要使用 std::ref 顯示說明(並且線程函數參數也需要聲明為引用).
void ThreadParamRef(std::string& str) { str += " --> add"; } void ThreadParam(std::string str) { str += " --> add"; } std::string str("Hello C++ Thread..."); //std::thread t(ThreadParamRef, str); std::thread t(ThreadParamRef, std::ref(str)); // 只有這種形式才能在線程執行完畢後輸出 Hello C++ Thread... --> add //std::thread t(ThreadParam, std::ref(str)); t.join(); std::cout << str << std::endl;
(2) 線程參數傳遞時需要注意不能傳入局部變量, 考慮下面的代碼,buffer②是一個指針變量,指向本地變量,然後本地變量通過buffer傳遞到新線程中②。
函數有很大的可能,會在字面值轉化成 std::string 對象之前崩潰,從而導致線程的一些未定義行為。
解決方案就是在傳遞到 std::thread 構造函數之前就將字面值轉化為 std::string 對象。
void f(int i,std::string const& s); void oops(int some_param) { char buffer[1024]; // 1 sprintf(buffer, "%i",some_param); std::thread t(f,3,buffer); // 2 t.detach(); } // 正確的方法 void f(int i,std::string const& s); void not_oops(int some_param) { char buffer[1024]; sprintf(buffer,"%i",some_param); std::thread t(f,3,std::string(buffer)); // 使用std::string,避免懸垂指針 t.detach(); }
(3) 線程函數參數傳遞時, 可以移動, 但不能拷貝. "移動"是指: 原始對象中的數據轉移給另一對象,而轉移的這些數據在原始對象中不再保存.
void ThreadParamUniquePtr(std::unique_ptr<int> up) { std::cout << (up.get() ? *up : -1) << std::endl; } std::thread t(ThreadParamUniquePtr, std::move(up)); //std::thread t(ThreadParamUniquePtr, up); // 不能編譯 //std::thread t(ThreadParamUniquePtr, std::ref(up)); // 要求線程函數參數也為引用才能編譯 t.join(); std::cout << (up.get() ? *up : -1) << std::endl; // 將輸出-1
線程是資源獨占型, 但可以將所有權轉移給別的對象. 如果一個 std::thread 對象與一個運行的線程關聯, 此時接受一個新的線程所有權時, 其以前關聯的線程將直接調用 std::terminate() 終止程序繼續運行.
std::thread t1(f);
std::thread t2(f);
// t1 = std::thread(f); // t1 所有權還沒有轉移, 不能通過賦一個新值來放棄線程
// t1 = std::move(t2); // t1 所有權還沒有轉移, 不能通過賦一個新值來放棄線程
t1.detach(); 或 t1.join();
t1 = std::move(t2);
std::thread t3 = std::move(t1);
t1 = std::move(t2);
線程對象也可以在函數中進行轉移.
std::thread f1()
{
return std::thread(f);
}
std::thread f2()
{
std::thread t(f);
return t;
}
void f3(std::thread t);
void f4()
{
f3(std::thread(f));
std::thread t(f);
f3(std::move(t));
}
由於 std::thread 是可轉移的, 如果容器對移動操作支持, 則可以將 std::thread 對象放入其中.
class Func { int i; public: Func(int i_) : i(i_) {} void operator() () { for (unsigned j = 0; j < 10; ++j) { std::cout << i << " "; } std::cout << std::endl; } }; std::vector<std::thread> threads; for (int i = 1; i < 10; i++) { Func f(i); //std::thread t(f); //v.push_back(t); // 不能采用這種方式 //v.push_back(std::move(t)); // 需要使用移動操作才可以 threads.push_back(std::thread(f)); } std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join)); // 對每個線程調用join()
std::thread::hardware_concurrency() 返回 CPU 核心線程數. 如果無法查詢系統信息時, 返回0. (static 函數)
get_id() 返回 std::thread 對象關聯的線程的 id. 如果所有權已轉移, 或線程函數已返回, 返回0.
std::this_thread::get_id() 取得當前線程的 id. (static 函數)
一個更好的ThreadGuard
#ifndef _THREAD_GUARD_ #define _THREAD_GUARD_ template <class _Thread> class ThreadGuard { public: explicit ThreadGuard(_Thread& t_) : t(t_) {} ~ThreadGuard() { if (t.joinable()) { t.join(); } } ThreadGuard(const ThreadGuard &) = delete; ThreadGuard& operator=(const ThreadGuard &) = delete; private: _Thread& t; }; #endif // _THREAD_GUARD_