yohhoyの日記

技術的メモをしていきたい日記

packaged_taskについて

C++11標準ライブラリで新しく追加されたstd::packaged_taskについてメモ。
関連記事:id:yohhoy:20120131, id:yohhoy:20120201

packaged_taskの基本

packaged_taskは関数オブジェクト*1を “非同期タスク” として保持するクラステンプレート。packaged_taskが提供する機能は、「std::functionによる関数オブジェクト保持+future/promiseによる同期機構と処理結果の受け渡し」のイメージが近い。

  • packaged_taskテンプレート引数には関数型のみ許容される。例:int型引数を1つとりdouble型を返す非同期タスクなら、packaged_task<double(int)>となる。
  • packaged_taskが参照するshared stateは、関数オブジェクトとその処理結果のみ保持する。packaged_taskでは関数オブジェクトに渡す実引数は保持しないことに注意。関数オブジェクトに対する実引数は、packaged_task::operator()呼び出しのときに渡す。(実引数の扱いはstd::functionと同じ考え方。)
  • 処理結果の設定は、関数オブジェクトから普通に値を返す(return)か例外送出(throw)すればよい。(std::promiseset_value/set_exception相当をライブラリ側が自動的に行う。)
  • 処理結果の取り出しはpackaged_taskオブジェクトから取り出したstd::futureを介して行う。futureオブジェクトは、packaged_task::get_futureメンバ関数呼び出しにて作成する。(処理結果の取り出しについてはfuture/promiseの関係と同じ。)

packaged_task単体ではスレッド生成は行なわないため、別スレッド上で非同期タスクを実行するためにstd::threadと組み合わせて利用する*2

#include <thread>
#include <future>

int main()
{
  // int型の引数を1つとってdouble型を返す非同期タスク
  std::packaged_task<double(int)> task([](int x) -> double {
    if (/*...*/) {
      double value = /* 何らかの計算 */
      return value;  // (2a) 処理結果を返す
    } else {
      throw std::exception();  // (2b) 例外throw
    }
  });
  std::future<double> f = task.get_future();

  std::thread th(std::move(task), 42);  // (1) 別スレッドで非同期タスクtaskを実行
 th.detach();

  /* 自スレッドでの処理 */;

  try {
    double result = f.get();  // (3a) 処理結果を取り出し(別スレッドでの処理完了を待機)
  } catch (...) {
    // (3b) 非同期タスク内でthrowされた例外が再throwされる
  }
  return 0;
}

実引数のバインド処理

double process(int x) { /*...*/ }

//-------- (A) packaged_task::operator()で実引数を渡す
const int arg = 42;
std::packaged_task<double(int)> task( process );
std::thread th( std::move(task), arg );

//-------- (B) 予め実引数をbindした関数オブジェクトを渡す
const int arg = 42;
std::packaged_task<double()> task( std::bind(process, arg) );
std::thread th( std::move(task) );

その他

  • get_futureメンバ関数で2回以上futureを取り出そうとすると、例外future_error/エラーコードfuture_already_retrievedが送出される。複数スレッドでfutureを共有するケースではstd::shared_futureを利用する。
  • operator()メンバ関数呼び出しによってshared stateが保持する関数オブジェクトを実行し、shared stateに処理結果が設定されてready状態となる。関数オブジェクトが例外を送出した場合は処理結果として扱われるため、operator()メンバ関数呼び出し自体は基本的に成功する。処理結果として格納された例外オブジェクトはfuture::get()呼び出しのときに再throwされる。
  • operator()メンバ関数呼び出しを2度以上行うと、同関数から例外future_error/エラーコードpromise_already_satisfiedが送出される。
  • 関数オブジェクトの実行と結果設定は行うが、取り出し可能とするタイミングをスレッド終了後まで遅延させるためにmake_ready_at_thread_exitメンバ関数が用意されている。同スレッド上のスレッドローカル変数(記憶クラス指定子にthread_localをつけた変数)が破棄された後に、shared stateがready状態に遷移する。

関連URL

*1:packaged_task は通常の関数を保持することもできる。本記事では通常の関数や呼出可能オブジェクト(callable object)をまとめて “関数オブジェクト” と表記する。

*2:シングルスレッドプログラム上で packaged_task を利用することも可能だが、特段のメリットが無いように思える。packaged_task::operator() による処理結果の生成を行わずに future::get を呼び出すと、処理結果生成を待機するため唯一のスレッドが永遠にブロック(デッドロック)される。