如果異步執行是必需的,指定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對象調用get或wait時——執行。那就是,執行會推遲到其中一個調用發生。當調用get或wait時,f
會同步執行,即,調用者會阻塞直到f
運行結束。如果get或wait沒有被調用,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
是否運行在——與調用get或wait函數的線程不同的——線程。如果那個線程是t,這句話的含義是沒有辦法預知f
是否會運行在與t不同的線程。 - 可能沒有辦法預知函數
f
是否執行完全,因爲沒有辦法保證fut
會調用get或wait。
默認發射策略的調度靈活性經常會混淆使用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能正常工作:
- 任務不需要與調用get或wait的線程併發執行。
- 修改哪個線程的thread_local變量都沒關係。
- 要麼保證std::async返回的future會調用get或wait,要麼你能接受任務可能永遠都不執行。
- 使用wait_for或wait_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發射策略。