iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
自我挑戰組

韌體工程師的不只 0x10 個問題系列 第 29

Day 29-用 async / promise + future 進行非同步處理

  • 分享至 

  • xImage
  •  

從 C++ 11 開始,C++ 可以用 async / promisefuture 實現非同步操作,本文將以超簡單範例說明相關用法,首先是較為簡潔的 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,參數為 34,如果沒這樣寫,那有可能走 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() 去拿背景執行緒的運算結果,共有兩種可能的情況:

  1. 背景執行緒已經運算完成:回傳結果 7 並可賦值於 ans;另一種可能是發生錯誤而回傳例外。
  2. 背景執行緒還沒算完:先卡住,等到完全算完才回傳結果並賦值於 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() 的運算結果 returnfuture,另外還有一種比較傳統的方法: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 變成透過 promiseset_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;
}

參考資料

  1. std::future in C++ (GeeksforGeeks)

上一篇
Day 28-記憶體模型
下一篇
Day 30-執行緒池
系列文
韌體工程師的不只 0x10 個問題31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言