從 C++ 11 開始,C++ 可以用 async / promise 跟 future 實現非同步操作,本文將以超簡單範例說明相關用法,首先是較為簡潔的 async + future,再來是仰賴更細緻手動處理的 promise + future。
async + future這個段落會先看到以下兩者的搭配:
async:啟動一個非同步任務,會回傳 future 物件。future:用來接 async 在背景運算好的結果或例外。用以下程式碼理解會更清楚:
#include <iostream>
#include <future>
#include <chrono>
#include <thread>
int slow_add(int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 用先停兩秒模擬慢工作
return a + b;
}
int main() {
// 啟動背景工作,回傳 future 物件,賦值於 f
std::future<int> f = std::async(std::launch::async, slow_add, 3, 4);
// 主執行緒先處理其他工作
std::cout << "Main thread does something else first...\n";
// 用 f.get() 得到背景工作結果,並賦值於 ans
int ans = f.get();
std::cout << "The answer is " << ans << "\n";
}
首先來觀察 main() 函數中的這一行:
std::future<int> f = std::async(std::launch::async, slow_add, 3, 4);
std::async 表示要開啟一個非同步執行的函式,會回傳一個 std::future 物件。
而在非同步任務中,使用 std::launch::async 表示「立刻」開新執行緒跑 slow_add,參數為 3 跟 4,如果沒這樣寫,那有可能走 std::launch::deferred 方式,表示等呼叫 future.get() 時才在原本的主執行緒執行,失去非同步意義。
而當背景執行緒處理緩慢的運算:
int slow_add(int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(2));
return a + b;
}
主執行緒會先處理其他工作:
std::cout << "Main thread does something else first...\n";
等需要結果的時候,主執行緒透過 f.get() 去拿背景執行緒的運算結果,共有兩種可能的情況:
7 並可賦值於 ans;另一種可能是發生錯誤而回傳例外。ans。執行結果如下:
Main thread does something else first... <- 主執行緒工作
The answer is 7 <-背景執行緒工作,等約兩秒才出現
剛剛是回傳結果成功被 f.get() 接住,但也可能出現例外情況,如下:
#include <iostream>
#include <future>
#include <chrono>
#include <thread>
#include <stdexcept>
int slow_add(int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(2));
// 新增例外情況
if (a < 0 || b < 0) {
throw std::runtime_error("Error: negative number detected"); // 拋出例外
}
return a + b;
}
int main() {
// 故意傳入 -3,觸發例外情況
std::future<int> f = std::async(std::launch::async, slow_add, -3, 4);
std::cout << "Main thread does something else first...\n";
// 因為 f.get() 可能會接到背景傳回來的例外,所以用 try-catch 包起來
try {
int ans = f.get(); // 這邊會發現遇到例外情況,跳到 catch 區塊
std::cout << "The answer is " << ans << "\n";
}
catch (const std::runtime_error& e) {
// 在主執行緒的 catch 區塊接住來自背景的例外
std::cout << "Error detected from main thread " << e.what() << "\n";
}
return 0;
}
promise + future剛剛用的 async 是把 slow_add() 的運算結果 return 給 future,另外還有一種比較傳統的方法:promise 也能把運算結果傳給 future,寫法如下:
#include <iostream>
#include <future>
#include <chrono>
#include <thread>
// 改用 promise 填值而沒有 return,因此回傳型別為 void,但參數多了 std::promise<int> prm
void slow_add(std::promise<int> prms, int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(2));
prms.set_value(a + b); // 從原本的 return 改成用 promise 回傳結果
}
int main() {
// 建立 promise,並從中取得連動的 future
std::promise<int> prms;
std::future<int> f = prms.get_future(); // future 物件會從 prms 的 get_future() 拿結果
// 啟動背景工作
// 手動開執行緒 t,並用 std::move 將 promise 物件由 main() 主執行緒轉移至 slow_add 背景執行緒
std::thread t(slow_add, std::move(prms), 3, 4);
std::cout << "Main thread does something else first...\n";
// 需要結果時再取出,若還沒算完則會等待
int ans = f.get();
std::cout << "The answer is " << ans << "\n";
// 必須加上這一行,等待背景執行緒安全結束
t.join();
}
首先來看 main() 函數,裡面建立了 promise 物件,下方則有 future 物件透過 .get_future() 方法取值:
std::promise<int> prms;
std::future<int> f = prms.get_future();
接著在啟動背景執行緒時,把 promise 透過參數傳給 slow_add():
std::thread t(slow_add, std::move(prms), 3, 4);
slow_add() 多接收一個 std::promise 參數,同時因為沒有 return,因此回傳型別為 void:
void slow_add(std::promise<int> prms, int a, int b)
回傳運算結果給 future 的方式由 return 變成透過 promise 的 set_value() 方法:
prms.set_value(a + b)
future 物件一樣透過 f.get() 取出結果,賦值於 ans:
int ans = f.get();
最後還必須確保主執行緒會等背景執行緒執行完,才繼續往下執行:
t.join();
執行結果一樣是:
Main thread does something else first... <- 主執行緒工作
The answer is 7 <-背景執行緒工作,等約兩秒才出現
若出現例外情況,寫法也會相對複雜,在背景執行緒跑的 slow_add() 函數也加上 try/catch 區塊:
#include <iostream>
#include <future>
#include <chrono>
#include <thread>
#include <stdexcept>
void slow_add(std::promise<int> prms, int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(2));
// 在背景執行緒中放入 try-catch 來捕捉錯誤
try {
if (a < 0 || b < 0) {
throw std::runtime_error("Error: negative number detected"); // 拋出例外
}
// 如果沒發生錯誤,就正常填入結果
prms.set_value(a + b);
}
catch (...) {
// 捕捉到任何例外後,利用 std::current_exception() 取得當前的錯誤指標
// 並透過 set_exception 把這個錯誤打包寄回給主執行緒的 future
prms.set_exception(std::current_exception());
}
}
int main() {
std::promise<int> prms;
std::future<int> f = prms.get_future();
// 故意傳入 -3 與 4,觸發例外情況
std::thread t(slow_add, std::move(prms), -3, 4);
std::cout << "Main thread does something else first...\n";
// 因為 f.get() 可能會接到背景傳回來的例外,所以用 try-catch 包起來
try {
int ans = f.get(); // 這邊會發現遇到例外情況,跳到 catch 區塊
std::cout << "The answer is " << ans << "\n";
}
catch (const std::runtime_error& e) {
// 成功在主執行緒的 catch 區塊接住來自背景的例外
std::cout << "Error detected from main thread: " << e.what() << "\n";
}
// 無論背景是成功還是失敗,都必須加上這一行來回收執行緒資源
t.join();
return 0;
}