深入探討 C++11 中引入的四個核心異步編程工具:std::async
, std::future
, std::promise
, 和 std::packaged_task
。它們共同構成了 C++ 現代并發編程的基礎。
為了更好地理解,我們可以使用一個餐廳點餐的類比:
std::future
(取餐憑證):這是你點餐后拿到的“小票”或“取餐號”。你拿著它,可以稍后去查詢(wait
)你的餐是否做好了,或者直接等待并取餐(get
)。這個憑證本身不能做餐,只能用來獲取最終的結果。std::async
(套餐服務):這是最省心的方式,就像點一個“全自動套餐”。你告訴柜臺你要什么(調用函數),系統(C++運行時)會自動安排一位廚師(一個新線程)去做,并直接給你一個取餐憑證(future
)。你什么都不用管,等著取就行了。std::promise
(后廚的承諾):這更像是后廚內部的溝通機制。假設一個廚師(生產者線程)向另一個服務員(消費者線程)承諾“我一定會把這道菜做出來”。廚師持有“承諾書”(promise
),可以在菜做好后把結果放進去。服務員則持有與這份承諾書配對的“取餐憑gingzheng”(future
)。這種方式將“承諾做”和“等待取”這兩個動作在不同線程中分離開來。std::packaged_task
(打包好的任務單):這就像一張標準化的“任務卡片”,上面寫明了要做什么菜(要執行的函數)以及附帶了一張可撕下的取餐憑證(future
)。你可以把這張任務卡片創建好,但先不交給任何廚師。之后,你可以把它交給任何一個有空的廚師(任何一個線程)去執行。這非常適合任務隊列和線程池的場景。
在深入探討之前,我們先執行一次搜索,以確保所有信息的準確性和時效性。
核心概念與關系
這四個工具都定義在 <future>
頭文件中,它們的核心目標是實現線程間的同步和數據傳遞。
std::future
是統一的“結果接收端”。 [1][2]std::async
,std::promise
,std::packaged_task
都可以看作是“結果的生產者”,它們都能創建一個與之關聯的std::future
對象,但創建和使用方式各不相同。 [3][4]
下面我們逐一詳細解析。
1. std::future
:未來的憑證
std::future
是一個對象,它代表了一個異步操作的最終結果。你可以把它想象成一個占位符,這個“坑”未來會被某個值或者一個異常填滿。
主要操作:
get()
: 等待異步操作完成,然后獲取其結果。這個函數會阻塞當前線程直到結果可用。注意:get()
只能被調用一次。wait()
: 阻塞當前線程,直到結果可用,但不獲取結果。wait_for()
,wait_until()
: 帶超時的等待。valid()
: 檢查future
是否與某個共享狀態相關聯(即是否有效)。
std::future
本身不啟動任何線程或任務,它純粹是用來接收結果的。
2. std::async
:最高級的異步任務啟動器
std::async
是一個函數模板,它的作用是啟動一個異步任務,并返回一個持有該任務結果的 std::future
。 [5] 這是最簡單、最直接的異步編程方式。
特點:
- 高度封裝: 你只需要提供一個可調用對象(如函數、lambda)及其參數,
std::async
會負責線程的創建和管理。 - 啟動策略 (Launch Policy): 這是
std::async
的一個關鍵特性,可以通過第一個參數指定: [6][7]std::launch::async
: 強制在一個新線程中立即異步執行任務。std::launch::deferred
: 延遲執行。任務不會立即開始,而是在你對返回的future
調用get()
或wait()
時,才在調用者的線程中同步執行。- 默認(不指定或使用
std::launch::async | std::launch::deferred
): 由C++運行時庫根據系統負載等情況自行決定是創建新線程還是延遲執行。
- 析構函數行為: 由
std::async
返回的std::future
對象,如果在其任務完成前被銷毀,其析構函數會阻塞直到任務執行完畢。 [3] 這是一個重要的陷阱,必須確保在future
銷毀前,其結果已經被獲取或等待。
用法示例:
#include <iostream>
#include <future>
#include <thread>
#include <chrono>int long_computation(int input) {std::cout << "Thinking..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));return input * 10;
}int main() {// 啟動一個異步任務std::future<int> result_future = std::async(std::launch::async, long_computation, 5);// 在主線程中做其他事情std::cout << "Main thread is doing other work." << std::endl;// 當需要結果時,調用get()// 這會阻塞,直到 long_computation 完成int result = result_future.get();std::cout << "The result is: " << result << std::endl;return 0;
}
3. std::promise
:一個明確的承諾
std::promise
提供了一種在線程間手動設置值或異常的機制。 [8][9] 它和 std::future
是一對一的“推-拉”關系:promise
負責“推”入一個值,而 future
負責“拉”取這個值。 [1][10]
特點:
- 解耦:
promise
將“設置值”的動作和“獲取值”的動作完全分離開來。你可以在一個線程中創建promise
和future
,然后將promise
移動到另一個線程去設置值。 - 顯式控制: 你可以精確控制何時、何地設置值(
set_value
)或異常(set_exception
)。 - 一次性使用: 每個
promise
只能設置一次值或異常。 [9]
用法示例:
#include <iostream>
#include <future>
#include <thread>
#include <string>void worker_thread(std::promise<std::string> p) {try {// 模擬一些工作std::this_thread::sleep_for(std::chrono::seconds(2));// 工作完成,兌現承諾p.set_value("Data from worker thread");} catch (...) {// 如果發生異常,設置異常p.set_exception(std::current_exception());}
}int main() {// 創建一個 promisestd::promise<std::string> data_promise;// 從 promise 獲取 futurestd::future<std::string> data_future = data_promise.get_future();// 啟動工作線程,并將 promise 移交給它// promise 不能被拷貝,只能移動std::thread t(worker_thread, std::move(data_promise));// 主線程做其他事情std::cout << "Main thread waiting for data..." << std::endl;// 等待并獲取結果std::string data = data_future.get();std::cout << "Received data: " << data << std::endl;t.join();return 0;
}
4. std::packaged_task
:打包好的待執行任務
std::packaged_task
是一個類模板,它包裝一個可調用對象(函數、lambda等),并允許其結果被異步地獲取。 [11][12] 它像是一個將“任務”和“獲取結果的憑證”捆綁在一起的包裹。
特點:
- 任務與執行分離:
packaged_task
將任務的定義和任務的執行分離開來。你可以先創建一個packaged_task
,在稍后的某個時間點,再把它交給一個線程去執行。 - 線程池的基石: 這個特性使得
packaged_task
成為實現線程池等任務隊列系統的理想工具。 [13][14] 你可以創建一個packaged_task
隊列,然后讓工作線程從中取出任務并執行。 - 自帶 Future: 創建
packaged_task
后,可以立即通過get_future()
方法獲取與之關聯的future
對象。 [12]
用法示例:
#include <iostream>
#include <future>
#include <thread>
#include <functional>
#include <vector>
#include <queue>int calculate_sum(int a, int b) {return a + b;
}int main() {// 1. 打包一個任務std::packaged_task<int(int, int)> task(calculate_sum);// 2. 獲取與任務關聯的 futurestd::future<int> result_future = task.get_future();// 3. 將任務移動到線程中執行// packaged_task 也不能被拷貝,只能移動std::thread t(std::move(task), 10, 20);// 主線程等待結果int result = result_future.get();std::cout << "The sum is: " << result << std::endl;t.join();return 0;
}
總結與對比
特性 | std::async | std::promise | std::packaged_task |
---|---|---|---|
抽象級別 | 高 | 低 | 中 |
核心作用 | 啟動一個異步任務并返回 future | 在線程間手動傳遞一個值或異常 | 包裝一個可調用對象,將其與 future 綁定 |
線程管理 | 自動 (由運行時庫決定) | 手動 (需要自己創建和管理線程) | 手動 (需要自己將任務對象傳遞給線程執行) |
耦合度 | 任務的調用和執行緊密耦合 | 值的“生產者”和“消費者”完全解耦 | 任務的“定義”和“執行”解耦 |
主要用例 | 簡單的“即發即忘”式異步調用 | 復雜的線程間通信,事件驅動模型 | 任務隊列,線程池實現 |
何時使用哪個?
-
優先選擇
std::async
: 如果你的需求僅僅是“在后臺運行這個函數,我稍后需要它的結果”,那么std::async
是最簡單、最安全、最推薦的選擇。它能避免很多手動管理線程的麻煩。 -
當需要精細控制時,使用
std::promise
: 如果你的結果不是由一個簡單的函數調用產生的,而是由一系列復雜的事件或計算決定的,或者當設置值的線程和獲取值的線程生命周期完全獨立時,std::promise
提供了所需的靈活性。 -
構建任務系統時,使用
std::packaged_task
: 如果你需要創建一個任務隊列,讓一組工作線程去處理,或者需要將任務的創建和執行分離開來(例如,在主線程中創建任務,在工作線程中執行),std::packaged_task
是最合適的構建塊。它是實現線程池的完美工具。 [13][15]
Learn more:
- cpp-notes/future-and-promise.md at master - GitHub
- 【C++并發編程】std::future、std::async、std::packaged_task與std::promise的深度探索(一)-阿里云開發者社區
- C++ 中async、packaged_task、promise 區別及使用原創 - CSDN博客
- C++ 并發操作的同步 - GuKaifeng’s Blog
- C++ async | how the async function is used in C++ with example? - EDUCBA
- async、packaged_task、promise、future的區別與使用 - L_B__
- std::async
- std::promise in C++ - GeeksforGeeks
- std::promise - cppreference.com
- Concurrency in C++ : Passing Data between Threads — Promise-Future - Medium
- C++ – 一文搞懂std::future、std::promise、std::packaged_task、std::async的使用和相互區別-StubbornHuang Blog
- Packaged Task | Advanced C++ (Multithreading & Multiprocessing) - GeeksforGeeks
- Making a Thread Pool in C++ from scratch - DEV Community
- Building a Thread Pool with C++ and STL - Coding Notes
- Getting Started With C++ Thread-Pool | by Bhushan Rane | Medium
補充:C++20引入了一些新的異步編程工具
a. 協程 (Coroutines)
這是最具革命性的變化,它引入了一種全新的異步編程模型。協程可以看作是可以暫停和恢復的函數。
核心理念:與 std::async 啟動一個可能阻塞的線程不同,協程可以在等待一個操作(如網絡I/O)時非阻塞地掛起自身,讓出執行權。當操作完成后,它可以從掛起的位置恢復執行。這使得單個線程能夠高效管理成千上萬的并發任務。
新關鍵字:引入了 co_await, co_yield, co_return 三個關鍵字來定義和控制協程的行為。
優勢:能夠以近似同步的方式編寫邏輯清晰的異步代碼,徹底告別“回調地獄”(Callback Hell)。 它非常適合I/O密集型應用,如高性能網絡服務器。
狀態:C++20 提供了協程的底層語言支持,但上層的高級封裝仍在發展中,通常需要配合像 Boost.Asio 這樣的庫來發揮最大威力。
b. std::jthread
std::jthread 是對 std::thread 的一個安全、現代的替代品。 [8]
自動 join: jthread 的析構函數會自動調用 join(),這意味著你不再需要手動管理線程的生命周期,從而避免了忘記 join() 或 detach() 導致的程序終止問題。這是它相比 std::thread 最顯著的優勢。
協作式中斷: jthread 內置了停止令牌 (stop token) 機制 (std::stop_source, std::stop_token)。你可以從外部請求一個 jthread 停止,而線程內部可以通過檢查 stop_token 的狀態來優雅地退出循環,實現了協作式的任務取消。
c. 同步原語:std::latch 和 std::barrier
這兩個工具用于協調多個線程的執行時機。
std::latch (門閂): 這是一個一次性的同步點。 你可以初始化一個計數器,多個線程到達后使計數器減一,當計數器減到零時,所有在 wait() 處等待的線程被同時喚醒。它非常適合“等待所有工作線程準備就緒后,一起開始執行任務”的場景。
std::barrier (屏障): 與 latch 類似,但它是可重用的。當所有線程都到達屏障點后,它們被釋放,然后屏障會自動重置,可用于下一輪同步。這非常適合迭代式算法,其中每一步計算都需要所有線程同步。