Effective Modern C++ 條款36 如果異步執行是必需的,指定std::launch::async策略

如果異步執行是必需的,指定std::launch::async策略

當你調用std::async來執行一個函數(或一個可執行對象)時,你通常希望函數是異步執行的。但你沒有要求std::async必須這樣做,函數是根據std::async的發射策略(launch policy)來執行的。有兩個標準策略,每個都是通過std::launch局部枚舉(scoped enum, 看條款10)來表示。假設一個函數f要傳遞給std::launch執行,

  • std::launch::async發射策略意味着函數f必須異步執行,即在另一線程執行。
  • std::launch::deferred發射策略意味着函數f可能只會在——std::async返回的future對象調用getwait時——執行。那就是,執行會推遲到其中一個調用發生。當調用getwait時,f會同步執行,即,調用者會阻塞直到f運行結束。如果getwait沒有被調用,f就絕對不會執行。

可能很奇怪,std::async的默認發射策略——它的默認策略是你不能顯式指定的——不是兩者其中的一種,相反,是兩者進行或運算。下面兩個函數完全是相同的意思:

auto fut1 = std::async(f);       // 使用默認發射策略執行f

auto fut2 = std::async(std::launch::async |     // 使用async或deferred執行f
                       std::launch::deferred
                       f);

默認的發射策略允許異步或同步執行函數f,就如條款35指出,這個靈活性讓std::async與標準庫的線程管理組件一起承擔線程創建和銷燬、避免過載、負責均衡的責任。這讓用std::async進行併發編程變得很方便。

但用std::async的默認發射策略會有一些有趣的含義。這語句給定一個線程t執行f

auto fut = std::async(f);        // 使用默認發射模式執行f
  • 沒有辦法預知函數f是否會和線程t併發執行,因爲f可能會被調度爲推遲執行。
  • 沒有辦法預知函數f是否運行在——與調用getwait函數的線程不同的——線程。如果那個線程是t,這句話的含義是沒有辦法預知f是否會運行在與t不同的線程。
  • 可能沒有辦法預知函數f是否執行完全,因爲沒有辦法保證fut會調用getwait

默認發射策略的調度靈活性經常會混淆使用thread_local變量,這意味着如果f寫或讀這種線程本地存儲(Thread Local Storage,TLS),預知取到哪個線程的本地變量是不可能的:

auto fut = std::async(f);         // f使用的線程本地存儲變量可能是獨立的線程的,
                                  // 也可能是fut調用get或wait的線程的

它也影響了基於wait循環中的超時情況,因爲對一個推遲(策略爲deferred)的任務(看條款35)調用wait_for或者wait_until會返回值std::launch::deferred。這意味着下面的循環,看起來最終會停止,但是,實際上可能會一直運行:

using namespace std::literals;         // 對於C++14的持續時間後綴,請看條款34

void f()           // f睡眠1秒後返回
{
    std::this_thread::sleep_for(1s);
}

auto fut = std::async(f);          // (概念上)異步執行f

while(fut.wait_for(100ms) !=         // 循環直到f執行結束
      std::future_status::ready)     // 但這可能永遠不會發生
{
    ...
}

如果f與調用std::async的線程併發執行(即,使用std::launch::async發射策略),這裏就沒有問題(假設f能結束執行,不會一直死循環)。但如果f被推遲(deferred),fut.wait_for將總是返回std::future_status::deferred。那永遠也不會等於std::future_status::ready,所以循環永遠不會終止。

這種bug在開發或單元測試中很容易被忽略,因爲它只會在機器負載很重時纔會顯現。在機器過載(oversubscription)或線程消耗完的狀況下,任務很可能會被推遲(如果使用的是默認發射策略)。總之,如果不是過載或者線程耗盡,運行系統沒有理由不調度任務併發執行。

解決辦法很簡單:檢查std::async返回的future,看它是否把任務推遲,然後呢,如果真的是那樣,就避免進入基於超時的循環。不幸的是,沒有辦法直接詢問future的任務是否被推遲。取而代之的是,你必須調用一個基於超時的函數——例如wait_for函數。在這種情況下,你不用等待任何事情,你只是要看看返回值是否爲std::future_status::deferred,所以請相信這迂迴的話語和用0來調用wait_for

auto fut = std::async(f);       // 如前

if (fut.wait_for(0) == std::future_status::deferred)  // 如果任務被推遲
{
    ...     // fut使用get或wait來同步調用f
} else {            // 任務沒有被推遲
    while(fut.wait_for(100ms) != 
         std::future_status::ready) {       // 不可能無限循環(假定f會結束)

      ...    // 任務沒有被推遲也沒有就緒,所以做一些併發的事情直到任務就緒
    }

    ...        // fut就緒
}

考慮多種因素的結論是,只要滿足了下面的條件,以默認發射策略對任務使用std::async能正常工作:

  • 任務不需要與調用getwait的線程併發執行。
  • 修改哪個線程的thread_local變量都沒關係。
  • 要麼保證std::async返回的future會調用getwait,要麼你能接受任務可能永遠都不執行。
  • 使用wait_forwait_unitil的代碼要考慮到任務推遲的可能性。

如果其中一個條件沒有滿足,你很可能是想要確保任務能異步執行。而那樣做的方法是,當你調用std::async時,把std::launch::async作爲第一個參數傳遞給它:

auto fut = std::async(std::launch::async, f);    // 異步發射f

事實上, 如果有一個函數的行爲像std::async那樣,但它會自動使用std::launch::async作爲發射策略,那樣就是一個方便的工作啦!它很容易寫出來,棒極了。這是C++11的版本:

template<typename F, typename... Ts>
inline
std::future<typename std::result_of<F(Ts...)>::type>
reallyAsync(F&& f, Ts&&... params)     // 返回異步調用f(param...)的future
{
    return std::async(std::launch::async,
                      std::forward<F>(f),
                      std::forward<Ts>(params)...);
}

這個函數接收一個可調用對象f和零個或多個參數params,並且把它們完美轉發(看條款25)給std::async,傳遞std::launch::async作爲發射策略。就像std::async那樣,它返回一個類型爲f調用params的結果的std::future,決定這個結果很容易,因爲std::result_of這個type trait可以把結果給你。

reallyAsync用起來就像std::async那樣:

auto fut = reallyAsync(f);    // 異步執行f,如果std::async拋異常reallyAsync也會拋異常

在C++14中,推斷reallyAsync返回值類型的能力簡化了函數聲明:

template<typename F, typename... Ts>
inline
auto
reallyAsync(F&& f, Ts&&... params)          // C++14
{
    return std::async(std::launch::async,
                      std::forward<F>(f),
                      std::forward<Ts>(params)...);
}

這個版本很清楚地讓你知道reallyAsync除了使用std::launch::async發射策略調用std::async外,沒做任何東西。


總結

需要記住的3點:

  • std::async的默認發射策略既允許任務異步執行,又允許任務同步執行。
  • 這個靈活性(上一點)導致了使用thread_local變量時的不確定性,它隱含着任務可能不會執行,它還影響了基於超時的wait調用的程序邏輯。
  • 如果異步執行是必需的,指定std::launch::async發射策略。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章