目錄
- 線程管理
- 啟動線程與(不)等待線程完成
- 特殊情況下的等待(使用trycath或rall)
- 后臺運行線程
線程管理
啟動線程與(不)等待線程完成
提供的函數對象被復制到新的線程的存儲空間中,函數對象的執行和調用都在線程的內存空間中進行。
class background_task
{
public:void operator()() const{do_something();do_something_else();}
};background_task f;std::thread my_thread(f);
注意,如果傳遞的是一個臨時變量,而不是一個命名變量,cpp編譯器會將其解析為函數聲明,而不是類型對象的定義。
當我們不等一個線程返回時,我們需要先將數據復制到線程中,這樣就不會產生訪問到已經銷毀的變量的問題了。
就如下所示,函數已經返回,線程依舊能夠訪問到局部變量
struct func
{int& i;func(int& i_) : i(i_) {}void operator() (){for (unsigned j=0 ; j<1000000 ; ++j){// 1 潛在訪問隱患:空引用do_something(i);}}
};
void oops()
{int some_local_state=0;func my_func(some_local_state);std::thread my_thread(my_func);my_thread.detach();
}
// 2 不等待線程結束
// 3 新線程可能還在運行
oops函數執行完成時,線程中的函數還在執行,還會訪問已經銷毀了的some_local_state變量,因為它持有的是該變量的指針。所以需要將數據復制到線程中,原始對象被銷毀也不妨礙線程中變涼了。當然,使用訪問局部變量的函數去創建線程不是很好。
此外,可以通過join()函數來確保線程在主函數完成前結束,可以確保局部變量在線程完成后才銷毀。
注意,只能對一個線程使用一次join,一旦使用過join,thread對象就不能再次匯入。
特殊情況下的等待(使用trycath或rall)
對于一個未銷毀的thread對象,如果想分離線程,在線程啟動后直接使用detach()進行分離。如果想等待線程,就需要思考好join位置,也要考慮拋出異常給join帶來的生命周期問題。
如下:使用了try/catch塊確保線程退出后函數才結束
struct func; //定義代碼上面有
void f()
{int some_local_state = 0;func my_func(some_local_state);std::thread t(my_func);try{do_something_in_current_thread();}catch(...){t.join();throw;}t.join();
}
接下來介紹使用RALL等待線程完成
class thread_guard
{std::thread& t;
public:explicit thread_guard(std::thread& t_): t(t_) {}~thread_guard(){if(t.joinable()) //1t.join(); //2}thread_guard(thread_guard const&) = delete; //3thread_guard& operator = (thread_guard const&) = delete;
};
struct func;void f()
{int some_local_state = 0;func my_func(some_local_state);std::thread t(my_func);thread_guard g(t);do_something_in_current_thread();
} //4
當線程執行到4,局部對象就要被逆序銷毀了。所以,對象g第一個被銷毀,線程在析構函數中,判斷是可join的,隨之執行join。所以即使do_something_in_current_thread函數跑出異常,這個銷毀依舊會發生。
還有個需要注意的地方,拷貝構造函數和拷貝賦值操作做標記為 =delete,編譯器不會自動生成。因為直接對對象進行拷貝或者賦值可能會丟失已經join的線程。
如果不想等待線程結束,可以分離線程,從而避免異常。不過這就打破了線程與std::thread對象聯系。即使線程仍然在后臺運行,detach操作也能確保std::terminate()在std::thread對象銷毀時才調用。
后臺運行線程
一個線程在后臺運行,就不能與主線程直接交互,分離的線程也不能join,不過c++保證,當線程退出時,相關資源能夠正確回收。
分離線程又稱守護線程。在UNIX中,守護線程指的是沒有任何顯式接口,并在后臺運行的線程。特點是長時間運行。
下面介紹一下分離線程的使用場景:
讓一個文字處理應用同時編輯多個文檔。每個文檔窗口看起來完全獨立,每個窗口也都有自己獨立的菜單選項,但他們卻運行在同一個應用實例中。一種內部處理方式是,讓每個文檔處理窗口擁有自己的線程。每個線程運行同樣代碼,并隔離不同窗口處理的數據。所以沒打開一個文檔就要啟動一個新線程,因為是對獨立文檔進行操作,所以沒有必要等待其他線程完成,可以讓文檔處理窗口運行在分離線程上。
void edit_document(std::string const& filename)
{open_document_and_display_gui(filename);while(!done_editing()){user_command cmd = get_user_input();if(cmd.type == open_new_document){std::string const new_name = get_filename_from_user();std::thread t(edit_document,new_name); //1t.detach(); //2}else{process_user_input(cmd);}}}
用戶選擇打開一個新文檔,需要啟動一個新線程去打開新文檔(如step1),并分離線程(如step2)。與當前線程做出的操作一樣,新線程只不過是打開另一個文件而已。所以,edit_document函數可以復用,并通過傳參的形式打開新的文件。