Heresy 在 2016 年寫的《C++11 程式的平行化:async 與 future》這篇文章,基本上算是很簡單地介紹 C++11 新加入的 std::async()
和 std::future<>
搭配使用的方法;而之前在《C++11 std::async 的運作分析》這篇文章,又大概分析了一下 std::async()
的運作模式。
而實際上,在 <future>
這個 header 裡面,除了 std::future<>
外,還有 std::promise<>
和 std::packaged_task<>
這兩個類別可以使用。
這兩者基本上都是用來產生 std::future<>
用的,其中 std::promise<>
應該比較像是用在底層實作,而 std::packaged_task<>
則是用來打包既有的函式、轉換回傳值的型式的。
這邊就先來整理一下 std::packaged_task<>
這個東西(參考)了。
基本使用
Package task 基本上是設計用來包裝一個可呼叫(callable)的目標,例如一個函式、lambda、function object 等等;透過他包裝後,之後就可以透過 std::future<>
的形式來取得執行的結果了。
概念上有點像是 std::async()
,但是 std::async()
會去實際執行、然後直接取得對應的 std::future<>
的物件,而 std::packaged_task<>
則是會產生一個新的可呼叫物件,之後要自己去執行他。
下面就是一個簡單的範例:
#include <iostream> #include <future> double compute(double val) { return val * val; } int main() { // build task std::packaged_task<double(double)> task(compute); // get future std::future<double> f = task.get_future(); // execute task(5); // get result std::cout << f.get() << std::endl; }
這邊一開始是先把 compute()
這個函式包裝成 std::packaged_task<double(double)>
,可以看到他的 template 引數的形式和 std::function<>
是一樣的。
之後,則是可以透過 get_future()
來取得對應的 std::future<double>
,作為之後讀取值之用。
而此時,compute()
是還沒有被執行的;要開始計算的話,則是要手動去呼叫包裝出來的 task
。
而 std::packaged_task<>
的 call operator 是不會回傳任何結果的,所以要取得 compute()
的結果,是要夠過 f
的 get()
才行。
多執行序
上面的例子是很簡單的示意,實際上真的這樣寫其實沒什麼意義。
這邊透過 std::packaged_task<>
來包裝既有的可呼叫目標的目的,是讓他可以在別的執行序執行、並透過 std::future<double>
來取得結果。
以上面的例子來說,如果想要另外開一個執行序來執行 compute()
的話,可以把程式改成:
#include <iostream> #include <future> #include <thread> double compute(double val) { return val * val; } int main() { // build task std::packaged_task<double(double)> task(compute); // get future std::future<double> f = task.get_future(); // execute in another thread std::jthread t(std::move(task), 5); // get result std::cout << f.get() << std::endl; }
實際上,上面的程式碼大致上會類似下面這樣直接使用 std::async()
的狀況:
std::future<double> f = std::async(std::launch::async, compute, 5); std::cout << f.get() << std::endl;
不過,就如同之前的測試、由於 std::async()
根據實作的不同,不一定會立刻開一個新的執行序來執行;這是上面兩種寫法在執行時的差別。
另外,這邊稍微要注意的是,由於 std::packaged_task<>
本身是不可複製的,所以在上面的例子裡面、在傳遞給 std::thread
的時候基本上是透過 std::move()
把所有權整個轉移給 std::thread
的物件、讓 t
這個執行序來管理 task
的生命週期。
而在主程式裡面,之後也就不能再去使用 task
這個物件了。
除了這樣把所有權整個轉移出去外,還有一個方法是使用參考的形式來傳遞;要這樣使用的話,只要把 std::move()
改成 std::ref()
就可以了。
std::jthread t(std::ref(task), 5);
不過由於是傳參考,所以就得自己顧好 task
的生命週期、不能讓他提早結束了。
重複使用
std::packaged_task<>
的物件在設計上是可以重複使用的,但是在重複使用的時候需要先呼叫 reset()
、同時也要重新取得新的 std::future<>
物件,其實有點麻煩。
下面是呼叫兩次的使用狀況:
// build task std::packaged_task<double(double)> task(compute); // first time std::future<double> f = task.get_future(); task(5); std::cout << f.get() << std::endl; // reset task.reset(); // second time f = task.get_future(); task(6); std::cout << f.get() << std::endl;
而如果是要寫成多執行序的話,大概可以寫成下面的形式:
// build task std::packaged_task<double(double)> task(compute); // first time std::future<double> f = task.get_future(); std::jthread t(std::ref(task), 5); std::cout << f.get() << std::endl; // reset task.reset(); // second time f = task.get_future(); t = std::jthread(std::ref(task), 6); std::cout << f.get() << std::endl;
這邊就一定得用參考的形式、使用 std::ref()
來傳遞了。
不過,考慮到 std::packaged_task<>
是有內部狀態、所以不可以同時呼叫多次的,所以總覺得意義好像不是很大?當然啦,可能也只是 Heresy 沒想到適用的狀況了。
大概就是這樣吧?基本上,直接使用 std::async()
來執行的話,是直接使用建置環境提供的實作來執行,像是 Visual Studio 就會去透過 thread pool 來執行。
而如果想要跳脫 std::async()
的實作設計的話,可能就是得自己建構 std::packaged_task<>
的物件來自己控制了。
像是有必要的話,也是可以自己寫一個類似 std::async()
的函式、但是固定開一個新的執行序來執行;下面就是一個例子:
#include <iostream> #include <future> #include <thread> template< class Function, class... Args > auto myAsync(Function&& f, Args&&... args) { std::packaged_task task(f); auto future = task.get_future(); std::thread(std::move(task), args...).detach(); return future; } int main() { auto f = myAsync([](int i) {return i * i; }, 3); std::cout << f.get() << std::endl; }
這樣的話,就可以相對簡單地替換掉本來的 std::async()
了。
而如果像是 g++ 和 clang 似乎是沒有使用 thread pool 的話,這邊其實也是可以自己實作自己的 thread pool 管理的。