C++11线程管理基础
1. 啟動線程
在C++ 11中線程是在std::thread對象創建時啟動。因為我們把啟動線程的重心放在如何構造這個thread對象,其構造函數有以下幾個:
//僅僅是構造一個線程類,但沒有和具體化的線程函數關聯 thread() noexcept; // 移動構造函數 thread( thread&& other ) noexcept; //構造新的 std::thread 對象并將它與執行線程關聯 template< class Function, class... Args > explicit thread( Function&& f, Args&&... args );其中f是任意一個可調用對象,包括普通函數、函數對象 、lambda表達式等,args是傳遞給線程函數的參數。
需要特別注意的是,傳到線程函數的參數默認是按值傳遞或者被移動的,若需要傳遞引用參數給線程函數,則必須包裝它(例如用 std::ref 或 std::cref )。
啟動線程的核心代碼:
//代碼清單 1 //函數對象 class background_task { public:background_task(int number, char* pszName) :m_nNumber(number),m_pszName(pszName){}~background_task(){}void operator()() const{std::this_thread::sleep_for(std::chrono::seconds(2));//分離式情況下,m_pszName是非法指針std::cout << "number is: " << m_nNumber << " name is: " << m_pszName << std::endl;} private:int m_nNumber;std::string m_strName;char* m_pszName; }; // t1 非線程 std::thread t1; //使用普通函數進行構造線程,按值傳遞 void f1(int n){std::cout << n << std::endl;} std::thread t2(f1, 10);//使用函數對象構造線程,使用引用 background_task task(12, "jimmy"); std::thread run_task(std::ref(task));2.等待線程完成或者分離
當我們啟動了一個線程,我們需要明確是要等待線程結束(加入式),還是讓其自主運行(分離式)。即線程對象退出之前必須明確調用join或detach,否則會崩潰。需要注意的是,必須在std::thread對象銷毀之前做出決定,否則你的程序將會終止(std::thread的析構函數會調用std::terminate(),這時再去決定會觸發相應異常)。如下代碼,既沒調用
join又沒detach,程序崩潰:
#include <memory> #include <string> #include <iostream> #include <thread>//代碼清單 1 //函數對象 class background_task { public:background_task(int number, char* pszName) :m_nNumber(number),m_pszName(pszName) {}~background_task() {}void operator()() const{std::this_thread::sleep_for(std::chrono::seconds(2));//分離式情況下,m_pszName是非法指針std::cout << "number is: " << m_nNumber << " name is: " << m_pszName << std::endl;} private:int m_nNumber;std::string m_strName;char* m_pszName; };//使用普通函數進行構造線程,按值傳遞 void f1(int n) { std::cout << n << std::endl; }int main() {// t1 非線程std::thread t1;std::thread t2(f1, 10);{//使用函數對象構造線程,使用引用/*char* p = const_cast<char*>("jimmy");*/char p[] = "jimmy";background_task task(12, p);/*std::thread run_task(std::ref(task));*/std::thread run_task(std::ref(task));}getchar();return 0; }如果線程是加入式的,我們需要調用std::thread的類成員函數join(),等待線程結束,然后再執行join()之后的代碼。
如果不等待線程,我們需要調用std::thread的類成員函數detach(),此時就必須保證線程結束之前,可訪問的數據得有效性。否則會產生未定義行為,或者不符合預期的值產生。這個是因為線程函數還持有函數局部變量的指針或者引用。
//代碼清單 2 //危險代碼示例 int _tmain(int argc, _TCHAR* argv[]) {{std::cout << "enter local zone..." << std::endl;char *buffer = new char[128]; sprintf(buffer, "%s", "this is thread...\n");//傳入指針,在函數對象中引用了指針background_task task(12, buffer);std::thread run_task(std::ref(task));//加入式,此時等到線程結束,指針一直有效//run_task.join();//分離式,主線程不等創建的新線程,指針提前釋放,存在非法行為run_task.detach();//釋放指針delete[] buffer;buffer = NULL;std::cout << "leave local zone..." << std::endl;}std::this_thread::sleep_for(std::chrono::seconds(5)); }運行結果:
加入式運行結果符合預期,分離式引用了非法內存值,結果異常,更嚴重時會因此崩潰形為。
3.后臺運行線程
當我們調用std::thread成員函數detach()后,會讓執行線程在后臺運行,此時主線程不能與之產生直接交互,并且不會等待這個線程執行結束,此時相關thread對象就無法被加入。
不過C++運行庫保證,當線程退出時,相關資源的能夠正確回收,后臺線程的歸屬和控制C++運行庫都會處理。
background_task task(12, buffer); std::thread run_task(std::ref(task)); run_task.detach(); //在清單2中增加get_id()以及joinalbe()測試接口 std::cout << "detach thread id : " << run_task.get_id() << std::endl; std::cout << "detach thread joinable status: " << run_task.joinable() << std::endl;?執行結果:
detach thread id : 0 detach thread joinable status: 0?當我調用detach()成員函數之后,相應的std::thread對象就與實際執行的線程無關了,就不能再調用join()成員函數,否則會拋異常。同時線程ID值也被設置成為0.
如下代碼:
int main() {// t1 非線程std::thread t1;std::thread t2(f1, 10);{//使用函數對象構造線程,使用引用/*char* p = const_cast<char*>("jimmy");*/char p[] = "jimmy";background_task task(12, p);/*std::thread run_task(std::ref(task));*/std::thread run_task(std::ref(task));run_task.detach();}getchar();return 0; }run_task出了大括號后被析構,但線程的執行函數對象不會因為對象被析構而崩潰、不執行?
4.特殊情況下的等待
如果我們應用程序必須等待某個線程,我們則需要細心挑選調用join()的位置。當在線程運行之后產生異常,在join()調用之前拋出或者返回,就意味著這次join()調用會被跳過,為了避免產生沒有調用join的情況,我們需要在可能的函數返回地方都加上join等待。
例如
int _tmain(int argc, _TCHAR* argv[]) {background_task task(12, "this is testing cpp");std::thread run_task(std::ref(task));try{do_other_something();}catch (...){run_task.join();return 1;}if (!check_user_intput()){run_task.join();return 1;}if (!process_user_data()){run_task.join();return 1;}run_task.join();return 0; }使用這種方式,雖然可以解決問題,但這種編碼方式是不可靠或者不簡潔的,如果后續新增代碼,異常處忘記添加join()等待就直接返回,則隱藏了bug,不利于后續問題定位。
我們希望無論正常與否,可以提供一個簡潔的機制,確保程序返回前都可以保障調用join(), 這種方式就叫RAII(資源獲取即初始化方式,Resource Acquisition Is Initialization).我們在提供一個類,在析構函數中使用join().
//線程類的RAII class thread_guard { public:explicit thread_guard(std::thread& t_) :t(t_){}~thread_guard(){if (t.joinable()) // 1 這個線程可以加入{t.join(); // 2等待線程結束std::cout << "thread is end..." << std::endl;}}thread_guard(thread_guard const&) = delete; // 3thread_guard& operator=(thread_guard const&) = delete;private:std::thread& t; }; //test code int _tmain(int argc, _TCHAR* argv[]) {background_task task(12, "this is testing cpp");std::thread run_task(std::ref(task));//thread_guard 對象析構后 會檢查run_task對象的是否可以加入,//如果可以加入則等待線程結束在析構自身,確保線程本身以簡潔的方式結束thread_guard guard(run_task);try{do_other_something();}catch (...){return 1;}if (!check_user_intput()){return 1;}if (!process_user_data()){return 1;}return 0; }在thread_guard的析構函數的測試中,首先判斷線程是否已加入①,如果沒有會調用join()②進行加入。這很重要,因為join()只能對給定的對象調用一次,所以對給已加入的線程再次進行加入操作時,將會導致錯誤。thread_guard保障線程無論如何都會等待線程結束。
總結
以上是生活随笔為你收集整理的C++11线程管理基础的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++11多线程thread参数传递问题
- 下一篇: 坐火车忘带身份证怎么办?手把手教你使用电