02 managing threads

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對象銷毀前沒有detachjoin,那么進(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ù)量(48線程返回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)決定挺峡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市担钮,隨后出現(xiàn)的幾起案子橱赠,更是在濱河造成了極大的恐慌,老刑警劉巖箫津,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狭姨,死亡現(xiàn)場離奇詭異,居然都是意外死亡苏遥,警方通過查閱死者的電腦和手機(jī)饼拍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來田炭,“玉大人师抄,你說我怎么就攤上這事〗塘颍” “怎么了叨吮?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瞬矩。 經(jīng)常有香客問我茶鉴,道長,這世上最難降的妖魔是什么景用? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任涵叮,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘围肥。我一直安慰自己剿干,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布穆刻。 她就那樣靜靜地躺著置尔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪氢伟。 梳的紋絲不亂的頭發(fā)上榜轿,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機(jī)與錄音朵锣,去河邊找鬼谬盐。 笑死,一個胖子當(dāng)著我的面吹牛诚些,可吹牛的內(nèi)容都是我干的飞傀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼诬烹,長吁一口氣:“原來是場噩夢啊……” “哼砸烦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起绞吁,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤幢痘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后家破,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體颜说,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年汰聋,在試婚紗的時候發(fā)現(xiàn)自己被綠了门粪。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡烹困,死狀恐怖庄拇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情韭邓,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布溶弟,位于F島的核電站女淑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏辜御。R本人自食惡果不足惜鸭你,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧袱巨,春花似錦阁谆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嫉入,卻和暖如春焰盗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背咒林。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工熬拒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人垫竞。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓澎粟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親欢瞪。 傳聞我的和親對象是個殘疾皇子活烙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容