假設(shè)要寫一個在后臺啟動線程的函數(shù),想通過新線程返回的所有權(quán)去調(diào)用這個函數(shù),而不是等待線程結(jié)束再去調(diào)用;或完全與之相反的想法:創(chuàng)建一個線程,并在函數(shù)中轉(zhuǎn)移所有權(quán),都必須要等待線程結(jié)束。總之,新線程的所有權(quán)都需要轉(zhuǎn)移。
這就是移動引入std::thread
的原因,C++標(biāo)準(zhǔn)庫中有很多資源占有(resource-owning)類型,比如std::ifstream
,std::unique_ptr
還有std::thread
都是可移動,但不可拷貝。這就說明執(zhí)行線程的所有權(quán)可以在std::thread
實例中移動,下面將展示一個例子。例子中,創(chuàng)建了兩個執(zhí)行線程,并且在std::thread
實例之間(t1,t2和t3)轉(zhuǎn)移所有權(quán):
void some_function();
void some_other_function();
std::thread t1(some_function); // 1
std::thread t2=std::move(t1); // 2
t1=std::thread(some_other_function); // 3
std::thread t3; // 4
t3=std::move(t2); // 5
t1=std::move(t3); // 6 賦值操作將使程序崩潰
當(dāng)顯式使用std::move()
創(chuàng)建t2后②,t1的所有權(quán)就轉(zhuǎn)移給了t2。之后,t1和執(zhí)行線程已經(jīng)沒有關(guān)聯(lián)了;執(zhí)行some_function的函數(shù)現(xiàn)在與t2關(guān)聯(lián)。
然后,與一個臨時std::thread
對象相關(guān)的線程啟動了③。為什么不顯式調(diào)用std::move()
轉(zhuǎn)移所有權(quán)呢?因為,所有者是一個臨時對象——移動操作將會隱式的調(diào)用。
t3使用默認(rèn)構(gòu)造方式創(chuàng)建④,與任何執(zhí)行線程都沒有關(guān)聯(lián)。調(diào)用std::move()
將與t2關(guān)聯(lián)線程的所有權(quán)轉(zhuǎn)移到t3中⑤。因為t2是一個命名對象,需要顯式的調(diào)用std::move()
。移動操作⑤完成后,t1與執(zhí)行some_other_function的線程相關(guān)聯(lián),t2與任何線程都無關(guān)聯(lián),t3與執(zhí)行some_function的線程相關(guān)聯(lián)。
最后一個移動操作,將some_function線程的所有權(quán)轉(zhuǎn)移⑥給t1。不過,t1已經(jīng)有了一個關(guān)聯(lián)的線程(執(zhí)行some_other_function的線程),所以這里系統(tǒng)直接調(diào)用std::terminate()
終止程序繼續(xù)運行。這樣做(不拋出異常,std::terminate()
是noexcept函數(shù))是為了保證與std::thread
的析構(gòu)函數(shù)的行為一致。2.1.1節(jié)中,需要在線程對象被析構(gòu)前,顯式的等待線程完成,或者分離它;進(jìn)行賦值時也需要滿足這些條件(說明:不能通過賦一個新值給std::thread
對象的方式來"丟棄"一個線程)。
std::thread
支持移動,就意味著線程的所有權(quán)可以在函數(shù)外進(jìn)行轉(zhuǎn)移,就如下面程序一樣。
清單2.5 函數(shù)返回std::thread
對象
std::thread f()
{
void some_function();
return std::thread(some_function);
}
std::thread g()
{
void some_other_function(int);
std::thread t(some_other_function,42);
return t;
}
當(dāng)所有權(quán)可以在函數(shù)內(nèi)部傳遞,就允許std::thread
實例可作為參數(shù)進(jìn)行傳遞,代碼如下:
void f(std::thread t);
void g()
{
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std::move(t));
}
std::thread
支持移動的好處是可以創(chuàng)建thread_guard類的實例(定義見 清單2.3),并且擁有其線程的所有權(quán)。當(dāng)thread_guard對象所持有的線程已經(jīng)被引用,移動操作就可以避免很多不必要的麻煩;這意味著,當(dāng)某個對象轉(zhuǎn)移了線程的所有權(quán)后,它就不能對線程進(jìn)行加入或分離。為了確保線程程序退出前完成,下面的代碼里定義了scoped_thread類?,F(xiàn)在,我們來看一下這段代碼:
清單2.6 scoped_thread的用法
class scoped_thread
{
std::thread t;
public:
explicit scoped_thread(std::thread t_): // 1
t(std::move(t_))
{
if(!t.joinable()) // 2
throw std::logic_error(“No thread”);
}
~scoped_thread()
{
t.join(); // 3
}
scoped_thread(scoped_thread const&)=delete;
scoped_thread& operator=(scoped_thread const&)=delete;
};
struct func; // 定義在清單2.1中
void f()
{
int some_local_state;
scoped_thread t(std::thread(func(some_local_state))); // 4
do_something_in_current_thread();
} // 5
與清單2.3相似,不過這里新線程是直接傳遞到scoped_thread中④,而非創(chuàng)建一個獨立的命名變量。當(dāng)主線程到達(dá)f()函數(shù)的末尾時,scoped_thread對象將會銷毀,然后加入③到的構(gòu)造函數(shù)①創(chuàng)建的線程對象中去。而在清單2.3中的thread_guard類,就要在析構(gòu)的時候檢查線程是否"可加入"。這里把檢查放在了構(gòu)造函數(shù)中②,并且當(dāng)線程不可加入時,拋出異常。
std::thread
對象的容器,如果這個容器是移動敏感的(比如,標(biāo)準(zhǔn)中的std::vector<>
),那么移動操作同樣適用于這些容器。了解這些后,就可以寫出類似清單2.7中的代碼,代碼量產(chǎn)了一些線程,并且等待它們結(jié)束。
清單2.7 量產(chǎn)線程,等待它們結(jié)束
void do_work(unsigned id);
void f()
{
std::vector<std::thread> threads;
for(unsigned i=0; i < 20; ++i)
{
threads.push_back(std::thread(do_work,i)); // 產(chǎn)生線程
}
std::for_each(threads.begin(),threads.end(),
std::mem_fn(&std::thread::join)); // 對每個線程調(diào)用join()
}
我們經(jīng)常需要線程去分割一個算法的總工作量,所以在算法結(jié)束的之前,所有的線程必須結(jié)束。清單2.7說明線程所做的工作都是獨立的,并且結(jié)果僅會受到共享數(shù)據(jù)的影響。如果f()有返回值,這個返回值就依賴于線程得到的結(jié)果。在寫入返回值之前,程序會檢查使用共享數(shù)據(jù)的線程是否終止。操作結(jié)果在不同線程中轉(zhuǎn)移的替代方案,我們會在第4章中再次討論。
將std::thread
放入std::vector
是向線程自動化管理邁出的第一步:并非為這些線程創(chuàng)建獨立的變量,并且將他們直接加入,可以把它們當(dāng)做一個組。創(chuàng)建一組線程(數(shù)量在運行時確定),可使得這一步邁的更大,而非像清單2.7那樣創(chuàng)建固定數(shù)量的線程。