Future 與非同步任務 (Futures and Asynchronous Tasks)
Overview Table
| 概念 | 重點 |
|---|---|
std::future<T> |
一次性事件的結果;只能移動;get() 只能取一次;就緒後不可重置 |
std::async |
啟動非同步任務並回傳 future;可用 std::launch 指定啟動策略 |
std::packaged_task<Sig> |
將 future 綁定到函式/可調用物件;本身可調用、可入佇列 → 任務調度器/執行緒池 |
std::promise<T> |
最底層:顯式 set_value() / set_exception() 設定結果 |
| 例外與 future | 任務拋出的例外存入 future,get() 時重拋;未設值即銷毀 → broken_promise |
std::shared_future |
可拷貝;每個執行緒持有自己的副本即可安全存取,無需加鎖 |
future:一次性事件的等待
如同在機場等待「登機廣播」:等待期間可以做其他事(看書、喝咖啡),但真正等的是那個一次性事件。C++ 標準庫以 future 為這種事件建模,皆宣告於 <future>:
| 型別 | 類比 | 特性 |
|---|---|---|
std::future<> |
std::unique_ptr |
只能與一個事件關聯;只能移動 |
std::shared_future<> |
std::shared_ptr |
多個實例可關聯同一事件;可拷貝 |
- 事件發生時 future 變為就緒 (ready),之後不可重置。
- 與資料無關的事件用
std::future<void>/std::shared_future<void>特化。 - 並行技術規範另有
std::experimental::future<>,加入持續性等新功能(見 04-Synchronizing-Operations/05-Continuations-Latches-and-Barriers)。
future 物件本身不提供同步存取
多個執行緒存取同一個 future 物件時,必須用互斥鎖等機制保護;但多個執行緒各自存取自己的 std::shared_future 副本(即使指向同一結果)則不需同步。
std::async 與啟動策略
std::thread 沒有接收回傳值的機制;std::async 啟動非同步任務並回傳持有結果的 std::future(代碼 4.6):
std::future<int> the_answer = std::async(find_the_answer_to_ltuae);
do_other_stuff();
std::cout << the_answer.get(); // 阻塞直到就緒,並取回結果
- 傳遞引數的方式與
std::thread相同:成員函式指標 + 物件指標(或std::ref)+ 其餘引數;右值以移動方式轉移 → 可使用只移動類型作為函式物件或引數(代碼 4.7)。
啟動策略以額外的第一個參數 std::launch 指定:
| 策略 | 行為 |
|---|---|
std::launch::async |
必須在獨立的新執行緒上執行 |
std::launch::deferred |
延遲到 wait() / get() 被呼叫時,才在呼叫端執行緒上執行 |
deferred | async(預設) |
由實作自行選擇兩者之一 |
deferred 的任務可能永不執行
預設策略允許實作選擇延遲執行;若函式呼叫被延遲,且從未對該 future 呼叫 wait() 或 get(),這個函式就永遠不會執行。需要保證併發時,必須明確指定 std::launch::async。
std::packaged_task<>:future 與任務綁定
std::packaged_task<> 將 future 綁定到函式或可調用物件——調用 packaged_task 就會調用底層函式,回傳值作為非同步結果存入 future。
- 模板參數是函式簽名,例如
std::packaged_task<std::stringvector<char>*, int>:- 簽名的回傳型別 → 決定
get_future()回傳的std::future<>型別 - 簽名的參數列表 → 決定其函式呼叫運算子的參數
- 型別可隱式轉換:以 int 參數、回傳 float 的函式即可建構
std::packaged_task<double(double)>
- 簽名的回傳型別 → 決定
- 本身是可調用物件:可包進
std::function、傳給std::thread,或直接呼叫。 - 用途:執行緒池(第9章)、任務調度器、把任務送到特定執行緒上執行——調度器只處理
std::packaged_task<>實例,不碰個別函式,將任務細節抽象化。
在執行緒間傳遞任務(GUI 範例,代碼 4.9 核心):
std::deque<std::packaged_task<void()>> tasks; // 受互斥鎖保護的任務佇列
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f) {
std::packaged_task<void()> task(f); // 打包任務
std::future<void> res = task.get_future(); // 先取得 future
std::lock_guard<std::mutex> lk(m);
tasks.push_backmove(task); // packaged_task 只能移動
return res; // 呼叫端可等待任務完成
}
GUI 執行緒在迴圈中從佇列取出 task 並執行 task();任務完成時對應的 future 變為就緒。
std::promise<>:顯式設值
當任務無法表達成單一函式呼叫、或結果來自多處時,使用最底層的 std::promise<T>:
promise.get_future()取得關聯的 future;set_value()設值後 future 立即就緒。- 典型情境(代碼 4.10):單執行緒處理多個網路連接——大量連接不宜一連接一執行緒(消耗資源、上下文切換頻繁),改由少數執行緒輪詢所有連接,封包以亂序抵達,每個封包 ID 對應一個 promise:
if (connection->has_incoming_data()) {
data_packet data = connection->incoming();
std::promise<payload_type>& p = connection->get_promise(data.id);
p.set_value(data.payload); // 對應的 future 立即就緒
}
將例外存入 future
同步呼叫會拋出例外的函式,改成非同步後例外也要能傳給呼叫端:
| 來源 | 例外如何進入 future |
|---|---|
std::async |
函式拋例外 → 存入 future、狀態置為就緒,get() 時重拋 |
std::packaged_task |
打包函式被調用時拋例外 → 同上 |
std::promise |
顯式呼叫 set_exception()(而非 set_value()),通常在 catch 區塊中 |
| 違背 promise | 未設值/未調用任務即銷毀 promise 或 packaged_task → future 存入 std::future_error,錯誤碼 std::future_errc::broken_promise |
try {
some_promise.set_value(calculate_value());
} catch (...) {
some_promise.set_exceptioncurrent_exception(); // 捕獲當前例外存入
}
- 例外型別已知時,可不拋出而直接存入:
set_exceptionlogic_error("foo"))——更清晰,且給編譯器更大最佳化空間。
兩個細節
- 重拋的例外是原始物件還是拷貝,標準未指定,各編譯器/庫實作不同。
- 書中的
std::copy_exception()是標準化前的名稱,正式進入標準後為std::make_exception_ptr()。
std::shared_future:多執行緒等待
std::future只能移動、結果只能get()一次;多執行緒無同步地存取同一個 future → 資料競爭與未定義行為。std::shared_future可拷貝,多個物件可引用同一共享狀態。- 各個 shared_future 物件上的成員函式呼叫彼此仍不同步 → 最佳做法:每個執行緒各持一份副本,各自透過自己的副本取結果即安全,無需加鎖(圖 4.1)。
三種建立方式:
std::shared_future<int> sfmove(f)); // 1 顯式移動;之後 !f.valid(
std::shared_future<std::string> sf2(p.get_future()); // 2 右值隱式轉移所有權
auto sf3 = p.get_future().share(); // 3 share() + auto 型別推導
- 應用場景:平行試算表——多個單元格的公式依賴同一單元格的終值,各任務透過 shared_future 引用該值,依賴就緒後即可平行計算。
┌─ std::async(f) ── 最省事:啟動非同步任務取回傳值
future 來源 ──┼─ std::packaged_task<Sig> ── 打包任務:自行決定何時、在哪個執行緒執行
└─ std::promise<T> ── 完全手動:顯式 set_value / set_exception
│
get_future()
▼
std::future<T> ──move / share()──▶ std::shared_future<T>
(只移動,get 一次) (可拷貝,每執行緒一份副本)
Exam/Test Patterns
| 情境/關鍵字 | 答案 |
|---|---|
| 「std::thread 拿不到回傳值」 | std::async + future.get() |
| 「async 的任務一直沒執行」 | 預設策略含 deferred(延遲到 get/wait);要保證新執行緒須 std::launch::async |
| 「執行緒池/任務調度的任務單元」 | std::packaged_task(future 綁定可調用物件) |
| 「結果來自多處、不能包成函式呼叫」 | std::promise 顯式 set_value |
| 「非同步任務拋出例外」 | 例外存入 future,get() 時重拋 |
| 「promise 沒設值就銷毀」 | std::future_error,錯誤碼 std::future_errc::broken_promise |
| 「多執行緒等待同一結果」 | std::shared_future,每執行緒一份副本(拷貝) |
| 「future 的 get() 想呼叫兩次」 | 不行:std::future 結果只能取一次 → 改用 shared_future |
Related Notes
- 04-Synchronizing-Operations/01-Condition-Variables-and-Thread-Safe-Queue
- 04-Synchronizing-Operations/03-Waiting-with-Time-Limits
- 04-Synchronizing-Operations/04-Functional-and-Message-Passing-Styles
- 04-Synchronizing-Operations/05-Continuations-Latches-and-Barriers
- 04-Synchronizing-Operations/Practice-Synchronizing-Operations
- 02-Thread-Management/02-Passing-Arguments-and-Transferring-Ownership
- 09-Advanced-Thread-Management/01-Thread-Pools