C++ Concurrency in Action 2nd Edition note
主線程由C++運(yùn)行時啟動。
2.1 基本線程管理
啟動一個線程(參數(shù)是函數(shù)):
void do_some_work();
std::thread my_thread(do_some_work);
?? std::thread
可以接收任何的可調(diào)用類型.(函數(shù)谆扎,函數(shù)指針巫糙,函數(shù)對象,lambda
表達(dá)式,functional
包裝,bind
)。函數(shù)對象的例子:
class background_task{
public:
void operator()()const{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
??此時讥珍,函數(shù)對象被拷貝到新創(chuàng)建線程的存儲中,并且被調(diào)用窄瘟。
??如果傳遞一個臨時對象而不是一個命名變量衷佃,那么在語法上和函數(shù)聲明的語法一樣,此時編譯器解釋為函數(shù)蹄葱,而不是一個對象的定義氏义。
std::thread my_thread(background_task());
??編譯器解釋為聲明一個接受單個參數(shù)(參數(shù)類型為指向一個沒有參數(shù)并且返回background_task
對象的函數(shù)指針)并且返回std::thread
對象的函數(shù)my_thread
锄列,而不是啟動一個新線程」哂疲可以通過傳入命名函數(shù)對象或者使用括號邻邮,或者使用統(tǒng)一初始化語法來避免:
std::thread my_thread((background_task()));
std::thread my_thread{background_task()};
??額外的括號防止將函數(shù)對象解釋為函數(shù)聲明。my_thread
聲明為std::thread
類型的對象克婶。
??lambda
表達(dá)式允許你寫一個局部函數(shù)筒严,在函數(shù)中捕獲局部變量,避免額外參數(shù)的傳遞情萤。
std::thread my_thread([]{
do_something();
do_something_else();
});
??必須在主線程結(jié)束之前顯式地join
或者detach
新線程鸭蛙。如果std::thread
對象銷毀前沒有detach
或join
,那么進(jìn)程將被終止(std::thread
析構(gòu)函數(shù)調(diào)用std::terminate()
).所以確保線程被join
或者被detach
(包括異常)很重要筋岛。
??如果detach
線程娶视,你需要確保線程要訪問的數(shù)據(jù)是一直有效的。
??一種情況是線程函數(shù)中包含指向局部變量的指針或者引用泉蝌,并且主線程(函數(shù))退出的時候線程還沒有退出(線程繼續(xù)訪問主線程函數(shù)中的局部變量)歇万。
struct func{
int&I;
func(int&i_):i(i_){}
void operator()(){
for(unsigned j=0;j<1000000;++j){
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();
}
??當(dāng)oops
退出的時候,my_thread
線程很可能還在運(yùn)行勋陪。如果線程還在運(yùn)行,下一次調(diào)用do_something(i)
的時候?qū)L問一個已經(jīng)銷毀的變量硫兰。
??一種解決方法是線程函數(shù)自包含并且將數(shù)據(jù)拷貝到線程而不是共享數(shù)據(jù)诅愚。如果使用可調(diào)用對象作為線程函數(shù),該對象將復(fù)制到線程劫映,原始對象可以立即銷毀违孝。但還是要注意包含指針或引用的對象。除非能夠保證線程在主線程函數(shù)退出之前完成泳赋。
??或者,在主線程函數(shù)退出前調(diào)用join
雌桑,保證線程的執(zhí)行完成。
??join()
清理任何線程相關(guān)的存儲祖今,所以std::thread
對象不再與現(xiàn)在完成的線程相關(guān)校坑,它(std::thread
對象)不與任何線程相關(guān)。只能對一個線程調(diào)用一次join()
千诬,如果已經(jīng)調(diào)用過join()
耍目,那么std::thread
對象不再可連接,joinable()
會返回false
徐绑。
??如果在線程開始之后join()
調(diào)用之前拋出異常邪驮,join()
的調(diào)用可能會被跳過。
??如果準(zhǔn)備在無異常的情況下調(diào)用join()
傲茄,那么還需要考慮在異常的情況下調(diào)用join()
:
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();
}
??使用try/catch
塊不是理想的辦法毅访。需要保證所有可能的出口路徑沮榜,都要調(diào)用join()
,無論正秤鞔猓或異常蟆融。
??一種方法是使用RAII
并且把join()
加到析構(gòu)函數(shù)。
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;
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();
}
??thread_guard
對象g
先銷毀磷斧,并且線程在析構(gòu)函數(shù)中連接振愿,即使do_something_in_current_thread
拋出異常導(dǎo)致函數(shù)退出。
??線程只能join()
一次弛饭。
??detach()
打斷線程和std::thread
對象的關(guān)聯(lián)冕末,并且保證當(dāng)std::thread
銷毀時不調(diào)用std::terminate()
,即使線程仍然在后臺中運(yùn)行侣颂。
??對std::thread
對象調(diào)用detach()
使線程在后臺運(yùn)行档桃,沒有任何直接的方式能與線程溝通。如果線程已經(jīng)分離憔晒,就不能再連接藻肄,因?yàn)闊o法獲得std::thread
對象。分離線程完全運(yùn)行在后臺拒担,所有權(quán)和控制移交到C++
運(yùn)行時庫嘹屯。線程退出時,C++
運(yùn)行時庫保證線程相關(guān)的資源正確回收从撼。
??分離的線程稱為守護(hù)線程州弟,守護(hù)線程運(yùn)行在后臺并且沒有任何顯式的用戶接口。守護(hù)線程通常是長時間運(yùn)行的低零。
??detach()
調(diào)用完成后婆翔,std::thread
對象不再與實(shí)際的線程關(guān)聯(lián),所以不再可連接掏婶。
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());
??從std::thread
對象分離線程啃奴,首先要有一個關(guān)聯(lián)線程。只有當(dāng)t.joinable()
返回true
時雄妥,才能對std::thread
對象調(diào)用t.detach()
最蕾。
2.2 線程函數(shù)傳入實(shí)參
??實(shí)參默認(rèn)拷貝到新創(chuàng)建線程的內(nèi)部存儲,然后像臨時對象那樣以右值傳遞到可調(diào)用對象或者函數(shù)茎芭。即使相應(yīng)的參數(shù)在函數(shù)中是引用的形式揖膜。
void f(int I,std::string const&s);
std::thread t(f,3,”hello”);
??即使f
的第二個形參是std::string
,實(shí)參傳進(jìn)來的是char const*
并且在新線程的上下文中轉(zhuǎn)換為std::string
梅桩。當(dāng)實(shí)參是一個指向局部變量的指針時:
void f(int i, std::string const&s);
void oops(int some_param){
char buffer[1024];
sprint(buffer,”%i”,some_param);
std::thread t(f,3,buffer);
t.detach();
}
??我們依賴隱式轉(zhuǎn)換將指向buffer
的指針轉(zhuǎn)換為std::string
對象壹粟,但是std::thread
的構(gòu)造函數(shù)只拷貝了指針的值,沒有將它轉(zhuǎn)換為我們需要的類型。
??oops
函數(shù)很可能在新線程將buffer
轉(zhuǎn)換為std::string
之前趁仙,就已經(jīng)退出了洪添,從而導(dǎo)致未定義行為。解決的辦法是將buffer
傳遞給std::thread
的構(gòu)造函數(shù)之前先將buffer
轉(zhuǎn)換為std::string
雀费。
void f(int i, std::string const&s);
void oops(int some_param){
char* buffer[1024];
sprint(buffer,”%i”,some_param);
std::thread t(f,3,std::string(buffer));
t.detach();
}
??非const
引用:
void update_date_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);
}
??std::thread
構(gòu)造函數(shù)拷貝了提供的數(shù)據(jù)干奢。但是為了處理只可移動類型,內(nèi)部代碼將該拷貝的實(shí)參作為右值傳遞盏袄,導(dǎo)致用一個右值調(diào)用update_data_for_widget
忿峻。這會編譯失敗,因?yàn)椴荒軅鬟f一個右值給一個非const
引用辕羽。你需要用std::ref
包裝一下這個實(shí)參:
std::thread t(update_data_for_widget, w, std::ref(data));
??如果提供一個合適的對象指針作為第一個實(shí)參推掸,那么可以傳遞成員函數(shù)指針作為線程函數(shù)熟妓。
class X{
public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work, &my_x);
??也可以為成員函數(shù)調(diào)用提供實(shí)參:std::thread
構(gòu)造函數(shù)的第三個實(shí)參作為成員函數(shù)的第一個實(shí)參薯嗤,以此類推豪墅。
??當(dāng)實(shí)參只可移動不可復(fù)制:源對象是臨時對象時,move
是自動調(diào)用的铣口;源對象是命名值時滤钱,需要調(diào)用std::move()
。
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object, std::move(p));
??big_object
的所有權(quán)先轉(zhuǎn)移到新線程的內(nèi)部存儲脑题,然后轉(zhuǎn)移到process_big_object
件缸。
??每一個std::thread
實(shí)例負(fù)責(zé)管理一個線程。線程的所有權(quán)可以在實(shí)例之間轉(zhuǎn)移叔遂,因?yàn)?code>std::thread實(shí)例是可移動的停团,但是不可復(fù)制。這確保任何時刻只有一個對象和線程關(guān)聯(lián)掏熬,同時允許程序員在對象之間轉(zhuǎn)移所有權(quán)。
2.3 線程所有權(quán)轉(zhuǎn)移
void some_function();
void some_other_function();
std::thread t1(some_function);
std::thread t2=std::move(t1);
t1=std::thread(some_other_function);
std::thread t3;
t3=std::move(t2);
t1=std::move(t3);
??最后一個move
將運(yùn)行some_function
的線程的所有權(quán)轉(zhuǎn)移回t1
秒梅。但是t1
已經(jīng)有一個關(guān)聯(lián)的線程(運(yùn)行some_other_function
)旗芬,所以std::terminate()
被調(diào)用來終止程序。這由std::thread
的析構(gòu)函數(shù)完成捆蜀。必須在線程析構(gòu)前顯式地等待線程完成或分離疮丛,賦值同理:你不能通過給std::thread
對象賦一個新值來丟棄一個線程。
??線程所有權(quán)可以轉(zhuǎn)移到函數(shù)外:
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;
}
??如果需要轉(zhuǎn)移所有權(quán)到一個函數(shù)中辆它,可以傳入std::thread
實(shí)例的值作為函數(shù)參數(shù)誊薄。
void f(std::thread t);
void g(){
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std;:move(t));
}
??可以使用thread_guard
類并取得線程的所有權(quán)。使用thread_guard
可以避免thread_guard
對象的生命周期比它引用的線程長锰茉,并且線程的所有權(quán)一旦轉(zhuǎn)移到對象呢蔫,那么其他對象就不能連接或分離線程。
class scoped_thread{
std::thread t;
public:
explicit scoped_thread(std::thread t_):t(std::move(t_)){
if(!t.joinable())
throw std::logic_error(“no thread”);
}
~scoped_thread(){
t.join();
}
scoped_thread(scoped_thread const&)=delete;
scoped_thread&operator=(scoped_thread const&)=delete;
};
struct func;
void f(){
int some_local_state;
scoped_thread t{std::thread(func(some_local_state))};
do_something_in_current_thread();
}
??thread_guard
類必須檢查線程是否可連接飒筑,scoped_thread
類是在構(gòu)造函數(shù)中檢查是否可連接片吊。如果不可連接绽昏,則拋出異常。(thread_guard
是在析構(gòu)函數(shù)中檢查俏脊,scoped_thread
是在構(gòu)造函數(shù)中檢查)全谤。
std::jthread
提供join()
的方式類似于scoped_thread
。
class joining_thread{
std::thread t;
public:
joining_thread()noexcept=default;
template<typename Callable, typename … Args>
explicit joining_thread(Callable&& func, Args&& …args):t(std::forward<Callable>(func), std::forward<Args>(args)…){}
explicit joining_thread(std::thread t_)noexcept:t(std::move(t_)){}
joining_thread(joining_thread&&other)noexcept:t(std::move(other.t)){}
joining_thread&operator=(joining_thread&&other)noexcept{
if(joinable()){
join();
}
t=std::move(other.t);
return *this;
}
joining_thread&operator=(std::thread other)noexcept{
if(joinable()){
join();
}
t=std::move(other);
return *this;
}
~joining_thread()noexcept{
if(joinable())
join();
}
void swap(joining_thread&other)noexcept{
t.swap(other.t);
}
std::thread::id get_id()const noexcept{
return t.get_id();
}
bool joinable() const noexcept{
return t.joinable();
}
void join(){
t.join();
}
void detach(){
t.detach();
}
std::thread& as_thread() noexcept{
return t;
}
const std::thread& as_thread() const noexcept{
return t;
}
};
??容器可以容納std::thread
對象:
void do_work(unsigned id);
void f(){
std::vector<std::thread> threads;
for(unsigned i=0;i<20;++i){
threads.emplace_back(do_work,i);
}
for(auto& entry:threads){
entry.join();
}
}
2.4 在運(yùn)行時選擇線程的數(shù)量
??std::thread::hardware_concurrency()
返回線程的數(shù)量(4
核8
線程返回8
)爷贫。
??std::accumulate
一個簡單的并行實(shí)現(xiàn)认然。它將工作分配給線程。每個線程有最低元素?cái)?shù)目漫萄,防止分配過多的線程卷员。
template<typename Iterator, typename T>
struct accumulate_block{
void operator()(Iterator first, Iterator last, T& result){
result=std::accumulate(first, last, result);
}
};
template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init){
unsigned long const length=std::distance(first, last);
if(!length)//empty
return init;
unsigned long const min_per_thread=25;
unsigned long const max_threads=(length+min_per_thread-1)/min_per_thread;
unsigned long const hardware_concurrency=std::thread::hardware_concurrency();
unsigned long const num_threads=std::min(hardware_threads!=0?hardware_threads:2, max_threads);
unsigned long const block_size=length/num_threads;
std::vector<T> results(num_threads);
std::vector<std::thread> threads(num_threads-1);
Iterator block_start=first;
for(unsigned long i=0;i<(num_threads-1);++i){
Iterator block_end=block_start;
std::advance(block_end,block_size);
threads[i]=std::thread(accumulate_block<Iterator,T>(),block_start,block_end,std::ref(results[i]));
block_start=block_end;
}
accumulate_block<Iterator, T>()(block_start,last,results[num_threads-1]);
for(auto& entry : threads){
entry.join();
}
return std::accumulate(results.begin(), results.end(), init);
}
2.5 標(biāo)識線程
??線程標(biāo)識的類型是std::thread::id
,可以通過std::thread::get_id()
或者std::this_thread::get_id()
取得卷胯。
??std::thread::id
類型的對象可以拷貝和比較子刮。
??標(biāo)準(zhǔn)庫提供std::hash<std::thread::id>
,使得std::thread::id
類型的值可以用于無序關(guān)聯(lián)容器中作為key
窑睁。
std::thread::id master_thread;
void some_core_part_of_algorithm(){
if(std::this_thread::get_id()==master_thread){
do_master_thread_work();
}
do_common_work();
}
??std::thread::id
實(shí)例可用于輸出流如std::cout
:
std::cout<<std::this_thread::get_id();
??具體的輸出由實(shí)現(xiàn)決定挺峡。