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_