前言
C++中在以往版本中不支持線程。需要使用pthread之類的使用線程。如果能夠使用C++自身的線程則可以使程序變得統一簡潔。
頭文件
- <thread> 該頭文件包含有std::thread類與std::this_thread類。以及管理線程的函數。是實現線程的主要文件。
- <atomic> 該頭文件包含有std::atomic和std::atomic_flag類,是實現原子操作的的主要文件。
- <mutex> 包含互斥相關的類與函數。
- <future> 包含有future類及相關的函數。
- <condition_variable> 包含有條件變量的類。
以上就是c++11 的線程部分了。雖然有關pthread與c++ thread有很多爭議,但是對于跨平臺的c++ thread來說,似乎更為標準一些。
推薦此書來學習c++11的線程。
https://www.gitbook.com/book/chenxiaowei/cpp_concurrency_in_action/details
hello world 線程
簡單認識thread類
#include <thread>
using namespace std;
// 下面這個函數是一個我們想要運行的線程任務
void hello()
{
printf("%s", "hello\n");
}
// 使用thread類,傳入一個函數作為我們的初始任務。不僅如此,還能傳入類等其他的參數
int main()
{
thread t(hello);
// 使用join來等待結束
t.join();
}
除了join來等待結束,可以使用detach來不等待線程結束。
struct func
{
int& i;
func(int& i_) : i(i_) {}
void operator() ()
{
for (unsigned j=0 ; j<1000000 ; ++j)
{
do_something(i); // 1. 潛在訪問隱患:懸空引用
}
}
};
void oops()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach(); // 2. 不等待線程結束
} // 3. 新線程可能還在運行
這個例子中,已經決定不等待線程結束(使用了detach()②),所以當oops()函數執行完成時③,新線程中的函數可能還在運行。如果線程還在運行,它就會去調用do_something(i)函數①,這時就會訪問已經銷毀的變量。如同一個單線程程序——允許在函數完成后繼續持有局部變量的指針或引用;當然,這從來就不是一個好主意——這種情況發生時,錯誤并不明顯,會使多線程更容易出錯。
如何等待線程結束?
如果需要等待線程,相關的std::thread實例需要使用join()。清單2.1中,將my_thread.detach()替換為my_thread.join(),就可以確保局部變量在線程完成后,才被銷毀。在這種情況下,因為原始線程在其生命周期中并沒有做什么事,使得用一個獨立的線程去執行函數變得收益甚微,但在實際編程中,原始線程要么有自己的工作要做;要么會啟動多個子線程來做一些有用的工作,并等待這些線程結束。
join()是簡單粗暴的等待線程完成或不等待。當你需要對等待中的線程有更靈活的控制時,比如,看一下某個線程是否結束,或者只等待一段時間(超過時間就判定為超時)。想要做到這些,你需要使用其他機制來完成,比如條件變量和期待(futures)。調用join()的行為,還清理了線程相關的存儲部分,這樣std::thread對象將不再與已經完成的線程有任何關聯。這意味著,只能對一個線程使用一次join();一旦已經使用過join(),std::thread對象就不能再次加入了,當對其使用joinable()時,將返回false。
向線程傳遞參數
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
代碼創建了一個調用f(3, "hello")的線程。注意,函數f需要一個std::string對象作為第二個參數,但這里使用的是字符串的字面值,也就是char const *類型。之后,在線程的上下文中完成字面值向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();
}
這種情況下,buffer②是一個指針變量,指向本地變量,然后本地變量通過buffer傳遞到新線程中②。并且,函數有很有可能會在字面值轉化成std::string對象之前崩潰(oops),從而導致一些未定義的行為。并且想要依賴隱式轉換將字面值轉換為函數期待的std::string對象,但因std::thread的構造函數會復制提供的變量,就只復制了沒有轉換成期望類型的字符串字面值。
解決方案就是在傳遞到std::thread構造函數之前就將字面值轉化為std::string對象:
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();
}
例如二:
還可能遇到相反的情況:期望傳遞一個引用,但整個對象被復制了。當線程更新一個引用傳遞的數據結構時,這種情況就可能發生,比如:
void update_data_for_widget(widget_id w,widget_data& data); // 1
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,data); // 2
display_status();
t.join();
process_widget_data(data); // 3
}
使用互斥
C++中通過實例化std::mutex創建互斥量,通過調用成員函數lock()進行上鎖,unlock()進行解鎖。不過,不推薦實踐中直接去調用成員函數,因為調用成員函數就意味著,必須記住在每個函數出口都要去調用unlock(),也包括異常的情況。C++標準庫為互斥量提供了一個RAII語法的模板類std::lock_guard,其會在構造的時候提供已鎖的互斥量,并在析構的時候進行解鎖,從而保證了一個已鎖的互斥量總是會被正確的解鎖。下面的程序清單中,展示了如何在多線程程序中,使用std::mutex構造的std::lock_guard實例,對一個列表進行訪問保護。std::mutex和std::lock_guard都在<mutex>頭文件中聲明。
#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list; // 1
std::mutex some_mutex; // 2
void add_to_list(int new_value)
{
std::lock_guard<std::mutex> guard(some_mutex); // 3
some_list.push_back(new_value);
}
bool list_contains(int value_to_find)
{
std::lock_guard<std::mutex> guard(some_mutex); // 4
return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();
}
當其中一個成員函數返回的是保護數據的指針或引用時,會破壞對數據的保護。具有訪問能力的指針或引用可以訪問(并可能修改)被保護的數據,而不會被互斥鎖限制?;コ饬勘Wo的數據需要對接口的設計相當謹慎,要確?;コ饬磕苕i住任何對保護數據的訪問,并且不留后門。
使用鎖
試想有一個玩具,這個玩具由兩部分組成,必須拿到這兩個部分,才能夠玩。例如,一個玩具鼓,需要一個鼓錘和一個鼓才能玩?,F在有兩個小孩,他們都很喜歡玩這個玩具。當其中一個孩子拿到了鼓和鼓錘時,那就可以盡情的玩耍了。當另一孩子想要玩,他就得等待另一孩子玩完才行。再試想,鼓和鼓錘被放在不同的玩具箱里,并且兩個孩子在同一時間里都想要去敲鼓。之后,他們就去玩具箱里面找這個鼓。其中一個找到了鼓,并且另外一個找到了鼓錘。現在問題就來了,除非其中一個孩子決定讓另一個先玩,他可以把自己的那部分給另外一個孩子;但當他們都緊握著自己所有的部分而不給予,那么這個鼓誰都沒法玩。
很幸運,C++標準庫有辦法解決這個問題,std::lock——可以一次性鎖住多個(兩個以上)的互斥量,并且沒有副作用(死鎖風險)。
如何避免死鎖?
避免嵌套鎖
第一個建議往往是最簡單的:一個線程已獲得一個鎖時,再別去獲取第二個。如果能堅持這個建議,因為每個線程只持有一個鎖,鎖上就不會產生死鎖。使用固定順序獲取鎖
當硬性條件要求你獲取兩個以上(包括兩個)的鎖,并且不能使用std::lock單獨操作來獲取它們;那么最好在每個線程上,用固定的順序獲取它們獲取它們(鎖)。
同步等待
假設你在旅游,而且正在一輛在夜間運行的火車上。在夜間,如何在正確的站點下車呢?一種方法是整晚都要醒著,然后注意到了哪一站。這樣,你就不會錯過你要到達的站點,但是這樣會讓你感到很疲倦。另外,你可以看一下時間表,估計一下火車到達目的地的時間,然后在一個稍早的時間點上設置鬧鈴,然后你就可以安心的睡會了。這個方法聽起來也很不錯,也沒有錯過你要下車的站點,但是當火車晚點的時候,你就要被過早的叫醒了。當然,鬧鐘的電池也可能會沒電了,并導致你睡過站。理想的方式是,無論是早或晚,只要當火車到站的時候,有人或其他東西能把你喚醒,就好了。
C++標準庫對條件變量有兩套實現:std::condition_variable和std::condition_variable_any。這兩個實現都包含在<condition_variable>頭文件的聲明中。兩者都需要與一個互斥量一起才能工作(互斥量是為了同步);前者僅限于與std::mutex一起工作,而后者可以和任何滿足最低標準的互斥量一起工作,從而加上了_any的后綴。因為std::condition_variable_any更加通用,這就可能從體積、性能,以及系統資源的使用方面產生額外的開銷,所以std::condition_variable一般作為首選的類型,當對靈活性有硬性要求時,我們才會去考慮std::condition_variable_any。