yohhoyの日記

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

futureとpromiseのあれこれ(理論編)

C++11標準ライブラリで新しく追加されたstd::promisestd::futureについてメモ。

future/promiseの基本

両者ともに標準ヘッダ <future> にて定義されるクラステンプレートであり、「別スレッドでの処理完了を待ち、その処理結果を取得する」といった非同期処理を実現するための部品*1

  • 処理結果として、通常の戻り値(value)または例外(exception)を扱う*2。戻り値の型はテンプレート引数にて指定するが、例外は任意の型を扱うことができる。(例: int型を扱うならstd::future<int>, std::promise<int>を用いる。例外はstd::exception_ptrを利用するため任意の型を伝搬可能。)
  • future は計算処理の完了待ち(同期機構)と結果取り出し(通信チャネル)機能を提供する。
  • promise は計算処理の結果設定(通信チャネル)と完了通知(同期機構)機能を提供する。
  • futureオブジェクトは、promiseオブジェクトのget_futureメンバ関数呼び出しにて作成する。
  • future, promiseオブジェクトともにコピー不可/ムーブ可能。

promiseオブジェクトとそこから取り出したfutureオブジェクトは内部的に同一の shared state を参照しており、この shared state を介して処理結果の受け渡しやスレッド間同期を実現する*3

#include <thread>
#include <future>

void func(std::promise<double> p, double x)
{
  try {
    double ret = /* 何らかの計算 */;
    p.set_value(ret);  // (2a) promiseに戻り値を設定
  } catch (...) {
    p.set_exception(std::current_exception());  // (2b) promiseに例外を設定
  }
}

int main()
{
  std::promise<double> p;
  std::future<double> f = p.get_future();

  double x = 3.14159;
  std::thread th(func, std::move(p), x);  // (1) 別スレッドで関数funcを実行

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

  try {
    double result = f.get();  // (3a) promiseに設定された値を取得(別スレッドでの処理完了を待機)
  } catch (...) {
    // (3b) promiseに設定された例外が再throwされる
  }

  th.join();  // (4) 別スレッドの完了待ち
  // future/promiseによって既に必要な同期はとられているが、thread::join()を呼ばずに
  // thオブジェクトのデストラクタが呼ばれると、std::terminate()が呼び出されてしまう。
  return 0;
}

future/promiseクラステンプレートの特殊化

future/promiseクラステンプレートでは、2つのテンプレート特殊化(lvalue reference型による部分特殊化, void型による特殊化)が提供される。promise::set_value()future::get()の引数/戻り値が異なる。

// R(プライマリテンプレート)
void promise::set_value(const R& r);
void promise::set_value(R&& r);
R future::get();
// R&
void promise<R&>::set_value(R& r);
R& future<R&>::get();
// void
void promise<void>::set_value();
void future<void>::get();

例外future_errorとエラーコード

future/promiseに対する操作でエラーが生じた場合、例外std::future_errorが送出される。このfuture_errorオブジェクトにはエラーコード(std::future_errc列挙型)が格納されており、例外の発生原因を確認できる。

標準ヘッダ <future> では、future関連のエラーコードとして下記4つを定義している。

  • broken_promise
  • future_already_retrieved
  • promise_already_satisfied
  • no_state
try {
  //...
} catch (const std::future_error& e) {
  if (e.code() == std::future_errc::no_state) {
    //...
  }
}

その他いろいろ

std::futureクラステンプレート

  • promise::get_future()で2回以上futureを取り出そうとすると、例外future_error/エラーコードfuture_already_retrievedが送出される。1つの計算結果を複数スレッドが待機&取得するケースのために、C++標準ライブラリでは std::shared_futureクラステンプレートを提供する。
  • 待機処理を行うwaitメンバ関数タイムアウト付き待機処理を行うwait_for, wait_untilメンバ関数を提供する。これらのメンバ関数getと異なり処理結果の取り出しを行わない。
  • futureメンバ関数同士は同期化されない。つまり同一futureオブジェクトに対して、異なるスレッドからそれぞれ操作するとデータレースを引き起こす。
  • shared stateを参照していないfutureオブジェクトのvaildメンバ関数falseを返す。このとき、同オブジェクトに対する操作*4は未定義(undefined)となっている。*5

std::promiseクラステンプレート

  • promiseへの結果設定はset_value/set_exceptionメンバ関数で行う。結果を設定するとshared stateはready状態となり、ふたたび結果設定を行おうとすると例外future_error/エラーコードpromise_already_satisfiedが送出される。
  • promiseへの結果設定は行うが取り出し可能とするタイミングをスレッド終了後まで遅延させる場合、set_value_at_thread_exit/set_exception_at_thread_exitメンバ関数が用意されている。同スレッド上のスレッドローカル変数(記憶クラス指定子にthread_localをつけた変数)が破棄された後に、shared stateがready状態に遷移する。*6
  • 結果を未設定のままpromiseオブジェクトが破棄されると、shared stateには例外future_error/エラーコードbroken_promiseが設定されてready状態となる*7

関連URL

*1:future/promise の組はブロッキング動作をするためマルチスレッドプログラム上での利用が前提となるが、future+async(deferred function) の組合わせはシングルスレッドプログラム上でも安全に利用できる。

*2:処理結果として扱えるのは、値または例外のいずれか一方。普通の関数が値を返す(return)か例外を投げる(throw)かの一方の動作しかできないのと同じ。

*3:"shared state" はC++標準規格上の用語であり、future/promise の振る舞いを定義するための概念的なもの。ユーザコードから shared state を直接操作することは出来ない。

*4:デストラクタ、ムーブ代入演算、validメンバ関数以外のメンバ関数呼び出し

*5:30.6.6/p3のNoteには「この状況で処理系は例外future_error/エラーコードno_stateを送出する」との記載があるが、C++標準規格は例外送出を要求していない(undefined)ため、処理系によっては異なる動作をする可能性もある。

*6:N2880, N3070によると、set_XXX_at_thread_exit メンバ関数は detached スレッド上のスレッドローカルオブジェクト破棄処理とのデータレースを防ぐために追加された。30.3.1.5/p10によれば「detach()済みスレッドが所有するリソースは、スレッド実行終了後に破棄しなければならない」と記載があるが、破棄タイミングについては定義しない。

*7:promiseオブジェクト破棄時に set_exception(std::make_exception_ptr( std::future_error(std::future_errc::broken_promise) )); 相当が行われる。