C++11 的 packaged_task

| | 0 Comments| 08:45
Categories:

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() 的結果,是要夠過 fget() 才行。


多執行序

上面的例子是很簡單的示意,實際上真的這樣寫其實沒什麼意義。

這邊透過 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 管理的。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *