yohhoyの日記

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

futureとshared_future

C++11標準ライブラリで新しく追加された std::shared_future についてメモ。id:yohhoy:20120131の続き。

shared_futureとfutureの関係

std::shared_futureは同一のshared stateを参照するコピー可能なクラステンプレート。複数スレッドから同じ処理結果を取得したいときに用いる*1。std::futureとstd::shared_futureの関係は、スマートポインタstd::unique_ptrとstd::shared_ptrの関係に似ている。

  • shared_futureオブジェクトは、futureオブジェクトからのムーブコンストラクトまたはfuture::share()メンバ関数にて作成する。同操作により、ムーブ元futureオブジェクトはshared stateを参照しなくなる。(future→shared_futureへの所有権の移動)
  • shared_futureオブジェクトをコピーすると、コピー先とコピー元は同じshared stateを参照するようになる。(future→shared_futureはコピー不可)
#include <thread>
#include <future>

void process(std::shared_future<int> f)
{
  // (b1) 処理
  int result = f.get();  // (b2)
  // (b3) resultを用いた処理
}

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

  std::thread th1(process, f), th2(process, f);

  int value = /* (a1) 何らかの計算 */;
  p.set_value(value);  // (a2)
  // (a3) 処理

  th1.join(); th2.join();
  return 0;
}

上記コードでは、メインスレッド/main関数と2つの別スレッド/process関数の間でスレッド間同期(a2)→(b2)*2をとって処理を行う。よって(a1)と(a2)、(a3)と(b3)はそれぞれ並行に実行されうるが、(b3)が(a1)より前に実行されることはない。ただし“(b1)より前に(a3)を実行”は起こりうる。(この場合でも(b1)はメインスレッドとは無関係に実行でき、(b3)の時点ではresultが求まっているので問題が無い。この実行順序が許容できないなら、そもそも異なる同期機構が必要な並行処理といえる。)

shared_futureクラステンプレートの特殊化

future/promise同様に、shared_futureクラステンプレートでも2つのテンプレート特殊化(lvalue reference型による部分特殊化版, void型による特殊化版)が提供される。特にgetメンバ関数において下記の差異がある。

  • future::getは非constメンバ関数だが、shared_future::getはconstメンバ関数となっている。
  • プライマリテンプレートにおいて、future::getはムーブ/コピー*3されたrvalueを、shared_future::getはshared state内部に保持された値へのconst lvalue referenceを返す。
// R(プライマリテンプレート)
const R& shared_future::get() const;
// R&
R& shared_future<R&>::get() const;
// void
void shared_future<void>::get() const;

処理結果へのアクセスとスレッド安全性

複数スレッドからshared_futureを介してshared stateが保持する処理結果にアクセスする場合、shared_futureでは特に何も保護しないためデータレースに注意しなければならない。

#include <map>
#include <thread>
#include <future>

typedef std::map<int, double> MapType;

void process(std::shared_future<MapType&> f)
{
  //...
  auto& map = f.get();

  map.insert(MapType::value_type(42, 3.14)); // ★危険!
}

int main()
{
  std::promise<MapType&> p;
  std::shared_future<MapType&> f = p.get_future().share();

  std::thread th1(process, f), th2(process, f);

  MapType map = /*...*/;
  p.set_value(map);

  th1.join(); th2.join();
  return 0;
}

上記コードでは、★箇所のmapへの要素追加処理でデータレースが生じてオブジェクトが壊れる(可能性がある)。オブジェクトを安全に変更するにはmutex等による排他制御が別途必要*4。仮に排他制御を行った場合この例では固定値を挿入するため必ず同じ結果が得られるが、各スレッドが“同一キーを持つ異なる値”を挿入する場合は非決定的(nondeterministic)な動作となりプログラム実行のたびに異なる結果を生成する。

N3337 30.6.4/p11, 30.6.7/p17より引用。

11 Access to the result of the same shared state may conflict (1.10). [ Note: this explicitly specifies that the result of the shared state is visible in the objects that reference this state in the sense of data race avoidance (17.6.5.9). For example, concurrent accesses through references returned by shared_future::get() (30.6.7) must either use read-only operations or provide additional synchronization. -- end note ]

17 Note: access to a value object stored in the shared state is unsynchronized, so programmers should apply only those operations on R that do not introduce a data race (1.10).

その他

std::futureは規格ドラフト段階ではstd::unique_futureという名前だった。N2997で名称変更が挙げられており、N3000以降のドラフトからはfutureに変更されている。C++11規格でもセクションラベルfutures.unique_futureにその名残がある。

C++11標準ライブラリにあわせ、Boost.Threadでも名称変更が提案されているようだが...
2012-11-08追記:Boost.Thread 1.50.0〜1.55.0まではboost::futureと選択可能。boost::unique_futureは廃止予定。→id:yohhoy:20120204

*1:std::futureはコピー不可のため、1スレッドしか処理結果を取得できない。futureオブジェクトへの参照またはポインタを複数スレッドで保持すれば処理結果の共有は実現できるが、shared stateが保持する処理結果オブジェクトの生存期間管理が困難となる。shared_futureを用いると、同一shared stateを参照するshared_futureオブジェクトが全て破棄されたタイミングで処理結果の破棄が保証される。

*2:スレッド間の同期処理(synchronize with)に関する厳密な定義は30.6.4/p9でなされている。

*3:戻り値型 R がムーブ代入可能(MoveAssignable)ならばムーブが、そうでなければコピーが行われる。

*4:全スレッドがオブジェクトに対する読み取り操作(read-only operation)しか行わないならば、排他制御を行わなくとも安全にアクセス可能。(30.6.4/p11)