程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> Effective Modern C++ 條款38 意識到線程句柄的析構函數的不同行為

Effective Modern C++ 條款38 意識到線程句柄的析構函數的不同行為

編輯:關於C++

意識到線程句柄的析構函數的不同行為

條款37解釋過一個可連接的(joinable)線程對應著一個底層的系統執行線程,一個非推遲任務(看條款36)的future和系統線程也有類似的關系。這樣的話,可以認為std::thread對象和future對象都可以操縱系統系統。

從這個角度看,std::thread對象和future對象的析構函數表現出不同的行為是很有趣的。就如條款37提到,銷毀一個可連接的std::thread對象會終止你的程序,因為另外兩個選擇——隱式join和隱式detach——被認為是更糟的選擇。而銷毀一個future,有時候會表現為隱式join,有時候會表現為隱式detach,有時候表現的行為既不是join也不是detach。它決不會導致程序終止,這種線程管理行為的方法值得我們仔細檢查。

我們從觀察一個future開始吧,它是一個交流管道的一端,在這個交流管道中被叫方要把結果傳給主叫方。被叫方(通常異步運行)把計算的結果寫進交流管道(通常借助一個std::promise對象),而主叫方使用一個future來讀取結果。你可以用下圖來思考,虛線箭頭展示了信息被叫這流向主叫:

這裡寫圖片描述

但被叫方的結果存儲在哪裡呢?在主叫方future執行get之前,被叫方可能已經執行完了,因此結果不能存儲在被叫的std::promise<喎?/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPsDvoaPEx7j2ttTP86Osu+HKx7G7vdC3vbXEvtayv7Hkwb+jrNTasbu90Na00NC94cr4uvO74bG7z/q72aGjPC9wPg0KPHA+yLu2+KOsveG5+9KysrvE3LTmtKLU2tb3vdC3vbXEZnV0dXJl1tCjrNLyzqqjqLu509DG5Mv71K3S8qOp0ru49jxzdHJvbmc+c3RkOjpmdXR1cmU8L3N0cm9uZz621M/zv8nE3LG708PAtLS0vajSu7j2PHN0cm9uZz5zdGQ6OnNoYXJlZF9mdXR1cmU8L3N0cm9uZz6jqNLytMuw0bG7vdC3vb3hufu1xMv509DIqLTTPHN0cm9uZz5zdGQ6OmZ1dHVyZTwvc3Ryb25nPteq0sa1vTxzdHJvbmc+c3RkOjpzaGFyZWRfZnV0dXJlPC9zdHJvbmc+o6mjrLb41NrX7tStyry1xDxzdHJvbmc+c3RkOjpmdXR1cmU8L3N0cm9uZz7P+rvZ1q6686Os1eK49jxzdHJvbmc+c3RkOjpzaGFyZWRfZnV0dXJlPC9zdHJvbmc+v8nE3Lvhsbu/vbG0uty24LTOoaPMyMj0sbu90Le9tcS94bn7wODQzcrHsru/ybG7v72xtLXEo6i8tNa7v8nSxravwODQzaOpo6y2+MTHveG5+8rH1rvSqtPQ0ru49mZ1dHVyZdL908PL/KOsy/y+zbvhtObU2qOsxMfDtKOstuC49mZ1dHVyZdbQxMTSu7j2uqzT0LG7vdC3vbXEveG5+8TYo788L3A+DQo8cD7S8s6qsbu90Le9ttTP87rN1ve90Le9ttTP87a8srvKyrrPtOa0or3hubmjrMv50tTV4rj2veG5+7Tm1NrBvdXf1q7N4rXEtdi3vaGj1eK49rXYt7290Nf2PHN0cm9uZz5zaGFyZWQgc3RhdGU8L3N0cm9uZz6jrDxzdHJvbmc+c2hhcmVkIHN0YXRlPC9zdHJvbmc+zaizo7Htz9bOqtK7uPa7+dPattHKtc/WtcS21M/zo6y1q7Hq17zDu9PQ1ri2qMv8tcTA4NDNoaK907/aus3Ktc/Wo6zL+dLUserXvL/itcTX99Xfv8nS1NPDy/vDx8+yu7a1xLe9t6jAtMq1z9Y8c3Ryb25nPnNoYXJlZCBzdGF0ZTwvc3Ryb25nPqGjPC9wPg0KPHA+yOfPwqOsztLDx7/J0tSw0db3vdChorG7vdChojxzdHJvbmc+c2hhcmVkIHN0YXRlPC9zdHJvbmc+1q685LXEudjPtcrTzby7r6Os0OnP37z9zbfU2bTOse3P1tDFz6K1xMH3z/KjujwvcD4NCjxwPjxpbWcgYWx0PQ=="這裡寫圖片描述" src="/uploadfile/Collfiles/20160916/20160916093610723.png" title="\" />

shared state的存在很重要,因為future的析構函數的行為——該條款的話題——是由與它關聯的shared state決定的。特別是:

最後一個引用shared state(它借助std::aysnc創建了一個非推遲任務時產生)的future的析構函數會阻塞直到任務完成。本質上,這種future的析構函數對底層異步執行任務的線程進行隱式的join其他的future對象的析構函數只是簡單地銷毀future對象。對於底層異步運行的任務,與對線程進行detach操作相似。對於最後一個future是推遲的任務的情況,這意味著推遲的任務將不會運行。

這些規則聽起來很復雜,但我們真正需要處理的是一個簡單“正常的”行為和一個單獨的例外而已。這正常的行為是:future的析構函數會銷毀future對象。那意思是,它不會join任何東西,也不會detach任何東西,它也沒有運行任何東西,它只是銷毀 future的成員變量。(好吧。實際上,它還多做了些東西。它減少了shared state裡的引用計數,這個shared state由future和被叫的std::promise共同操控。引用計數可以讓庫知道什麼時候銷毀**shared state,關於引用計數的通用知識,請看條款19.)

對於正常行為的那個例外,只有在future滿足下面全部條件才會出現:

future引用的shared state是在調用了std::async時被創建任務的發射策略是std::launch::async(看條款36),要麼是運行時系統選擇的,要麼是調用std::async時指定的。 這個future是最後一個引用shared state的future。對於std::future,它總是最後一個,而對於std::shared_future,當它們被銷毀的時候,如果它們不是最後一個引用shared state的future,那麼它們會表現出正常的行為(即,銷毀成員變量)。

只有當這些條件都被滿足時,future的析構函數才會表現出特殊的行為,而這行為是:阻塞直到異步運行的任務結束。特別說明一下,這相當於對運行著std::async創建的任務的線程執行隱式join

這個例外對於正常的future析構函數行為來說,可以總結為“來自std::async的future在它們的析構函數裡阻塞了。”對於初步近似,它是正確的,但有時候你需要的比初步近似要多,現在你已經知道了它所有的真相了。

你可能又有另一種疑問,可能是“我好奇為什麼會有這麼奇怪的規則?”。這是個合理的問題,關於這個我只能告訴你,標准委員會想要避免隱式detach引發的問題(看條款37),但是他們又不想用原來的策略讓程序終止(針對可連接的線程,看條款37),所以他們對隱式join妥協了。這個決定不是沒有爭議的,他們也有討論過要在C++14中禁止這種行為。但最後,沒有改變,所以future析構函數的行為在C++11和C++14相同。

future的API沒有提供方法判斷future引用的shared state是否產生於std::async調用,所以給定任意的future對象,不可能知道它的析構函數是否會阻塞到異步執行任務的結束。這有一些有趣的含義:

// 這個容器的析構函數可能會阻塞
// 因為包含的future有可能引用了借助std::async發射的推遲任務的而產生的shared state
std::vector> futs;           // 關於std::future,請看條款39

class Widget {                // Widget對象的析構函數可能會阻塞
public:
    ...
private:
    std::shared_future fut;
};

當然,如果你有辦法知道給定的future不滿足觸發特殊析構行為的條件(例如,通過程序邏輯),你就可以斷定future不會阻塞在它的析構函數。例如,只有在std::async調用時出現的shared state才具有特殊行為的資格,但是有其他方法可以創建shared state。一個是std::packaged_task的使用,一個std::packaged_task對象包裝一個可調用的對象,並且允許異步執行並獲取該可調用對象產生的結果,這個結果就被放在shared state裡。引用shared state的future可以借助std::packaged_taskget_future函數獲取:

int calcValue();         // 需要運行的函數

std::packaged_task pt(calcValue);   // 包裝calcValue,因此它可以異步允許

auto fut = pt.get_future();     // 得到pt的future

在這時,我們知道future對象fut沒有引用由std::async調用的產生的shared state,所以它的析構函數將會表現出正常的行為。

一旦std::packaged_task對象pt被創建,它就會被運行在線程中。(它也可以借助std::async調用,但是如果你想要用std::async運行一個任務,沒有理由創建一個std::packaged_task對象,因為std::async能做std::packaged_task能做的任何事情。)

std::packaged_task不能被拷貝,所以當把pt傳遞給一個std::thread構造函數時,它一定要被轉換成一個右值(借助std::move——看條款23):

std::thread t(std::move(pt));             // 在t上運行pt

這個例子讓我們看到了一些future正常析構行為,但如果把這些語句放在同一個塊中,就更容易看出來:

{           // 塊開始
    std::packaged_task pt(calcValue);

    auto fut = pt.get_future();

    std::thread t(std::move(pt));

   ...    // 看下面
}         // 塊結束

這裡最有趣的代碼是“…”,它在塊結束之前,t創建之後。這裡有趣的地方是在“…”中,t會發生什麼。有3個基本的可能:

t什麼都沒做。在這種情況下,t在作用域結束時是可連接的(joinable),這將會導致程序終止(看條款37)。 t進行了join操作。在這種情況下,fut就不需要在析構時阻塞了,因為代碼已經join了。 t進行了detach操作。在這種情況下,fut就不需要在析構時detach了,因為代碼已經做了這件事了。

換句話說,當你shared state對應的future是由std::packaged_task產生的,通常不需要采用特殊析構策略,因為操縱運行std::packaged_taskstd::thread的代碼會在終止、joindetach之間做出決定。
*需要記住的2點:

future的析構函數通常只是銷毀future的成員變量。 最後一個引用shared state(它是在借助std::aysnc創建了一個非推遲任務時產生)的future會阻塞到任務完成。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved