前一篇基本上針對 C++11 <future>
的 std::packaged_task<>
做了簡單的說明了,接下來,則來簡單紀錄一下 std::promise<>
(C++ Reference)吧。
首先,在 Heresy 來看,std::promise<>
基本上應該是 std::async()
和 std::packaged_task<>
內部實作時使用的類別;它是設計成可以用來儲存一個值或是例外,然後之後可以透過對應的 std::future<>
來取得資料的類別。
在使用上,取得值的方法很單純,只有 get_future()
這個函式;透過這個函式可以取得 std::promise<>
對應的 std::future<>
物件,可以用來確認 std::promise<>
的狀態、並取得他的值。而如果 std::promise<>
物件是被設定成例外狀況的話,那在呼叫 std::future<>
的 get()
的時候、就會丟出裡面儲存的例外。
而設定 std::promise<>
的值則有兩種,一種是 set_value()
、另一種是 set_exception()
。前者基本上就是程式運作正常,把計算的結果儲存到 std::promise<>
裡讓對應的 std::future<>
物件可以透過 get()
拿到結果;後者就是在處理過程有問題,所以自這邊設定一個例外狀況、讓之後要取得結果的時候可以收到造成錯誤的例外。
如果要把之前介紹 std::packaged_task<>
時的例子改成用 std::promise<>
的話,在不使用 thread 的時候大概會像下面這樣:
#include <iostream> #include <future> double compute(double val) { return val * val; } int main() { // create promise std::promise<double> p; // get future std::future<double> f = p.get_future(); // set value p.set_value(compute(5)); // get value std::cout << f.get() << std::endl; }
而如果要在另一個執行序做的話,則可以把呼叫 set_value()
的地方改成:
// set value in another thread std::jthread t( [&p]() { p.set_value(compute(5)); });
如果這邊不想透過 lambda 再包一層的,那就需要直接修改 compute()
了。修改過的程式大致上會像下面的樣子:
#include <iostream> #include <future> #include <thread> void compute(std::promise<double> p, double val) { p.set_value(val * val); } int main() { // create promise std::promise<double> p; // get future std::future<double> f = p.get_future(); // set value in another thread std::jthread t(compute, std::move(p), 5); // get value std::cout << f.get() << std::endl; }
這邊在基本的範例來看,基本上就是用 std::promise<>
的 set_value()
取代函式裡面的 return
,但是實際上也不是所有情境都是這樣就是了。
實際上,這邊也可以使用多個 std::promise<>
、或是在 set_value()
後繼續做別的工作;不過,這種情境大概是比較複雜的狀況才會需要了。
至於 set_exception()
該怎麼用呢?基本上,會需要這個功能,主要就是一般狀況下,在額外的執行序丟出來的例外(exception)是沒辦法直接透過 try – catch 抓到的;而如果是透過 std::future<>
的設計的話,則是可以在呼叫 get()
的時候、抓到過程中丟出的例外。
不過真的要能做到,其實就是要在執行的時候去攔截例外、並透過 set_exception()
來告訴 std::promise<>
才行。
比如說上面的程式如果改寫成下面的狀況:
#include <iostream> #include <future> #include <thread> double compute(double val) { if (val < 0) throw std::runtime_error("should not >= 0"); return val * val; } int main() { // create promise std::promise<double> p; // get future std::future<double> f = p.get_future(); try { // set value in another thread std::jthread t( [&p]() { p.set_value(compute(-1)); }); // get value std::cout << f.get() << std::endl; } catch (const std::exception& e) { std::cerr << e.what() << "\n"; } }
在 compute()
裡面,如果傳入的參數比 0 小就會丟出例外。而這邊是傳 -1 進去,感覺應該要攔截到丟出來的例外吧?但是實際上這邊因為另外是在另一個執行序產生的,所以主執行序的 try – catch 並沒有辦法攔截到它。
如果要讓他可以攔截到 compute()
丟出來的例外的話,以這個例子來說,是需要在 lambda 裡面額外處理的。這邊比較簡單是可以改成:
// set value in another thread std::jthread t( [&p]() { try { p.set_value(compute(-1)); } catch (...) { p.set_exception(std::current_exception()); } });
這邊基本上就是去檢查在正常執行的流程中有沒有例外,有的話就把現在的例外透過 set_exception()
丟給 std::promise<>
;這樣一來,之後在呼叫 f.get()
的時候,他就會把內部儲存的例外再重新丟出來,這樣就可以正確攔截到例外了!
不過這邊算是省略了一些東西啦~實際上考量到 p
本身的狀態,set_exception()
本身也是可能丟例外出來要另外再接的;所以真的要安全的話,在上面的 catch 裡面應該還得再 try-catch 一次。
另外,除了直接使用 set_X()
來設定外,他也提供了 set_X_at_thread_exit()
的版本,會在執行序結束的時候去設定值。
不過老實說,個人不太清楚這東西什麼時候會用到就是了…
而如果真的要用的話,要注意的是他會在執行序結束的時候、變數都刪除後才會執行;所以如果寫成:
void compute(std::promise<double> p, double val) { p.set_value_at_thread_exit(val * val); }
這樣的話,在最後要設定值的時候會因為 p
已經被消滅了而失敗。
解決方法是用改用參考的形式:
#include <iostream> #include <future> #include <thread> void compute(std::promise<double>& p, double val) { p.set_value_at_thread_exit(val * val); } int main() { std::promise<double> p; std::future<double> f = p.get_future(); std::jthread t(compute, std::ref(p), 5); std::cout << f.get() << std::endl; }
理論上這樣應該是沒問題的。但是就是得自己管理好 p
的生命週期了。
std::promise<>
的基本使用大概就是這樣了?感覺上,在大部分的狀況下,似乎沒有特別去直接使用他的需求?
但是如果是想在把多個 std::future<>
對應的 std::promise<>
都丟到同一個執行序的時候,或許會是有用的?只是到底什麼時候會有這種需求,個人也還沒想到了。 XD