網(wǎng)站版面布局結(jié)構(gòu)武漢百度百科
1 線程基本管控
每個(gè)C++程序都含有至少一個(gè)線程,即運(yùn)行main()的線程,它由C++運(yùn)行時(shí)系統(tǒng)啟動(dòng)。隨后程序可以發(fā)起更多線程,它們以別的函數(shù)作為入口。這些新線程連同起始線程并發(fā)運(yùn)行。當(dāng)main()返回時(shí),程序就會(huì)退出;同樣,當(dāng)入口函數(shù)返回時(shí),對(duì)應(yīng)的線程隨之終結(jié)。如果借std::thread對(duì)象管控線程,即可選擇等他結(jié)束。
1.1 發(fā)起線程
線程通過(guò)構(gòu)造std::thread對(duì)象而啟動(dòng),該對(duì)象指明線程要運(yùn)行的任務(wù)。
void do_some_work();
std::thread myThread(do_some_work);
任何可調(diào)用類型都適用于std::thread。所以,作為代替,可以設(shè)計(jì)一個(gè)帶有函數(shù)調(diào)用操作符的類(應(yīng)當(dāng)是下面的operator)
class background_task
{
public:void operator() () const{do_something();do_something_else();}
};background_task f;
std::thread my_thread(f);
f被復(fù)制到屬于新線程的存儲(chǔ)空間中,在那里被調(diào)用,由新線程執(zhí)行。
1.1.1 與函數(shù)聲明進(jìn)行區(qū)分
如果傳入std::thread的是臨時(shí)變量,不是具名變量,那么調(diào)用構(gòu)造函數(shù)的語(yǔ)法有可能與函數(shù)聲明相同。這種情況,編譯器會(huì)將其解釋成函數(shù)聲明。
聲明為函數(shù):函數(shù)名為my_thread,只接收一個(gè)參數(shù),返回std::thread對(duì)象
std::thread my_thread(background_task());可以通過(guò)多用一對(duì)圓括號(hào)或使用新式的統(tǒng)一初始化語(yǔ)法
std::thread my_thread((background_task()));
std::thread my_thread{background_task()};還可以使用lambda表達(dá)式
std::thread my_thread([]{do_something();do_something_else();
});
1.2 匯合與分離
在啟動(dòng)線程后,需要明確是要等待他結(jié)束(也就是匯合)還是任由他獨(dú)立運(yùn)行(也就是分離)。如果等到std::thread銷毀的時(shí)候還沒(méi)有決定好,那么std::thread的析構(gòu)函數(shù)將調(diào)用std::terminate()終止整個(gè)程序。
如果選擇了分離,且分離時(shí)新線程還未運(yùn)行結(jié)束,那將繼續(xù)運(yùn)行,甚至在std::thread對(duì)象銷毀很久之后依然運(yùn)行,它只有最終從線程函數(shù)返回時(shí)才會(huì)結(jié)束運(yùn)行。
假設(shè)程序不等待線程結(jié)束,那么在線程運(yùn)行結(jié)束前,我們要保證它所訪問(wèn)的外部數(shù)據(jù)始終正確,有效。由于使用多線程,所以我們可能會(huì)經(jīng)常面臨對(duì)象生存期的問(wèn)題。比如下面的案例:
struct func
{int& i;func(int& i_):i(i_){}void operator() (){for (unsigned j=0; j<1000000; ++j) {do_something(i); 隱患:可能訪問(wèn)懸空引用}}
};void oops()
{int some_local_state=0;func my_func(some_local_state);std::thread my_thread(my_func);my_thread.detach(); 不等待新線程結(jié)束新線程可能仍運(yùn)行,而主線程的函數(shù)卻已經(jīng)結(jié)束
}
主線程 | 新線程 |
構(gòu)造my_func對(duì)象,引用局部變量some_local_state | |
通過(guò)my_thread對(duì)象啟動(dòng)新線程 | |
新線程啟動(dòng) | |
調(diào)用func::operator() | |
分離新線程my_thread | 運(yùn)行func::operator(); 調(diào)用do_something()函數(shù), 進(jìn)而引用局部變量some_local_state |
銷毀局部變量some_local_state | 仍在運(yùn)行 |
退出oops() | 繼續(xù)運(yùn)行func::operator(); 調(diào)用do_something()函數(shù), 進(jìn)而引用some_local_state, 導(dǎo)致未定義行為 |
因此:以下做法不可取:意圖在函數(shù)中創(chuàng)建線程,并讓線程訪問(wèn)函數(shù)的局部變量。除非線程肯定會(huì)在該函數(shù)退出前結(jié)束。或者是匯合新線程,此舉可以保證在主線程的函數(shù)退出前,新線程執(zhí)行完畢。
1.2.1 join—等待線程完成
若需等待線程完成,那么可以在與之關(guān)聯(lián)的std::thread實(shí)例上,通過(guò)調(diào)用成員函數(shù)join()實(shí)現(xiàn)。對(duì)于上面的代碼,就是把detach換成join。就能夠保證在oops退出前,新線程結(jié)束。
對(duì)于一個(gè)線程,join僅能被調(diào)用一次,被調(diào)用后線程不再可匯合,成員函數(shù)joinable將返回false。
要注意,如果線程啟動(dòng)后有異常拋出,而join尚未執(zhí)行,該join調(diào)用會(huì)被略過(guò)。
使用thread_guard保證在拋出異常時(shí),退出路徑的先后順序與不拋出異常時(shí)一致。
也就是在析構(gòu)函數(shù)中調(diào)用join
class thread_guard {std::thread& t;
public:explicit thread_guard(std::thread& t_) : t(t_){}~thread_guard() {if (t.joinable()) {t.join();}}thread_guard(thread_guard const&)=delete;thread_guard& operator=(thread_guard const&)=delete;
};struct func {int& i;explicit func(int& i_) : i(i_) {};void operator() () {for (unsigned j = 0; j < 1000000; ++j) {do_somthing();}}
};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();
}
1.2.2 detach—在后臺(tái)運(yùn)行線程
會(huì)令線程在后臺(tái)運(yùn)行,因此與之無(wú)法直接通信。其歸屬權(quán)和控制權(quán)都交給了C++運(yùn)行時(shí)庫(kù),由此保證,一旦線程退出,與之關(guān)聯(lián)的資源都會(huì)被正確回收。
只有在joinable返回true時(shí),才能調(diào)用detach。
2 向線程函數(shù)傳遞參數(shù)
直接向std::thread的構(gòu)造函數(shù)增添更多參數(shù)即可。需要注意的是,線程具有內(nèi)部存儲(chǔ)空間,參數(shù)會(huì)按照默認(rèn)方式先復(fù)制到該處,新創(chuàng)建的執(zhí)行線程才能直接訪問(wèn)它們。然后,這些副本被當(dāng)成臨時(shí)變量,以右值的形式傳給新線程上的函數(shù)或可調(diào)用對(duì)象。即便函數(shù)相關(guān)參數(shù)按設(shè)想應(yīng)該是引用,上述過(guò)程依然會(huì)發(fā)生。
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");void f(int i, std::string const& s);
void oops(int some_param)
{char buffer[1024];sprintf(buffer, "%i", some_param);std::thread t(f, 3, buffer);// std::thread t(f, 3, std::string(buffer));t.detach();
}
但是上述例子將字符串的引用復(fù)制到了thread的存儲(chǔ)空間,當(dāng)調(diào)用thread的外層函數(shù)銷毀時(shí),buffer將不存在,無(wú)法訪問(wèn)這個(gè)引用。可以使用注釋里的方法,先轉(zhuǎn)換成std::string對(duì)象(buffer相當(dāng)于一個(gè)指針)
void update_data_for_widget(widget_id w, widget_data& data);
void oops_again(widget_id w)
{widget_data data;std::thread t(update_data_for_widget, w, data);display_status();t.join();process_widget_data(data);
}
根據(jù)update_data_for_widget函數(shù)的聲明,第二個(gè)參數(shù)會(huì)以引用的方式傳入update_data_for_widget,但是std::thread的構(gòu)造函數(shù)并不知情,會(huì)直接復(fù)制提供的值。隨后線程庫(kù)內(nèi)部會(huì)把參數(shù)副本當(dāng)成move-only(只移型別),以右值的形式傳遞。最終,update_data_for_widget會(huì)收到右值,因?yàn)閡pdate_data_for_widget預(yù)期接受非const引用,我們不能向他傳遞右值。
解決方法是,按照如下方式改寫(xiě)(std::ref)
std::thread t(update_data_for_widget, w, std::ref(data));
這樣就保證了傳入update_data_for_widget函數(shù)的不是變量data的臨時(shí)副本,而是指向變量data的引用,因此能夠編譯成功。
2.2 調(diào)用對(duì)象的方法
若要調(diào)用一個(gè)對(duì)象對(duì)應(yīng)的方法,則需要傳遞方法地址和對(duì)象地址,第三個(gè)參數(shù)作為該方法的第一個(gè)入?yún)ⅰ?/p>
class X {
public:void do_lengthy_work();};
X my_x;
std::thread t(&X::do_lengthy_work, &my_x);
上述代碼調(diào)用對(duì)象my_x的do_lengthy_work方法。