yohhoyの日記

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

boost::synchronized_valueクラス

Boost.Thread 1.54.0で実験的に追加された boost::synchronized_value<T> クラステンプレートについてメモ。

要約:

  • 型Tに対して、複数スレッド間での暗黙的な排他アクセス インタフェースを提供するラッパクラス。synchronized_value<T>クラスは、boost::mutex*1とT型の値を内包するスマートポインタのように振る舞う。
  • 不可分(atomic)な複合操作や複数オブジェクト操作のために、明示的な排他アクセス インタフェースも提供される。(synchronizeメンバ関数+可変個引数boost::synchronizeメンバ関数

注意:公式ドキュメントにて、実験的機能のため将来バージョンで仕様変更が予想されること、現時点ではテストが不十分であることが明言されている。また実装をみると非ドキュメント機能がいくつか散見される。(Boost.Thread 1.55.0現在)

基本的な使い方

下記コードにてthread1thread2が別スレッドから同時に呼ばれるとき、変数s0に対する操作はデータ競合(data race)による未定義動作(undefined behavior)を引き起こすが、変数s1経由での操作は排他制御されるため正しく動作する(空文字列または"abc"のいずれかが出力される)。

#include <string>
#include <iostream>
#include <boost/thread/synchronized_value.hpp>

void print(const std::string& s) { std::cout << s << std::endl; }

std::string plain_s0;
boost::synchronized_value<std::string> sync_s1;

void thread1()
{
  plain_s0.append("abc");  // NG: thread2の参照操作とデータ競合
  sync_s1->append("abc");  // OK
}

void thread2()
{
  print(plain_s0);  // NG: thread1の変更操作とデータ競合
  print(*sync_s1);  // OK
  // or print(sync_s1.get());
}

排他アクセスの対象型Tがプリミティブ型(intbool等)の場合は、オーバーヘッドの小さいBoost.AtomicライブラリやC++11標準ライブラリ提供のatomic<T>利用を検討すべき*2。例:スレッドセーフなフラグ変数やカウンタ変数程度なら、atomic<bool>atomic<int>で十分足りるはず。

メモ:synchronized_value<T>にあるexplicit operator T() constメンバの存在価値をいまいち理解できていない。C++11以降で下記v0,v1スタイルを許容するのが目的か?

boost::synchronized_value<T> s;
// C++11
T v0( s );  // OK
T v1{ s };  // OK
T v2 = s;   // NG

// C++03
T v0a( *s );      // OK
T v0b( s.get() ); // OK
T v2a = *s;       // OK
T v2b = s.get();  // OK

トランザクション:単一オブジェクト

単一synchronized_value<T>オブジェクトに対する複合操作を不可分実行したい場合(=トランザクション)、synchronizeメンバ関数を用いてプロキシオブジェクトを取り出せる。プロキシオブジェクト取得時点でsynchronized_value<T>内部ロックを獲得済みとなっており、同変数のスコープ終了時に自動的に内部ロックが解放される。プロキシオブジェクト経由での値操作はsynchronized_value<T>オブジェクトとほぼ同様となる(スマートポインタ的に振る舞う)。

boost::synchronized_value<std::string> sync_str;

void threadN_bad()
{
  if (sync_str->empty()) {
    // NG: このタイミングではロック解放されるため、他スレッド操作により
    //     if文中にて確認した条件式を満足しなくなっている可能性がある。
    sync_str->append("abc");
  }
}

void threadN_good()
{
  auto proxy = sync_str.synchronize();
  if (proxy->empty()) {
    proxy->append("abc");
  }
  // OK: proxy変数スコープ=ロック獲得期間となるため、
  //     ここまでに他スレッドがsync_strを変更することはない。
}

注意:プロキシオブジェクト生存期間中に直接synchronized_value<T>オブジェクト経由で値操作を行うと、同一スレッドから内部ロックを複数回獲得することになる。既定で用いられるboost::mutexクラスでは、このような再帰的ロック獲得操作は許容されず未定義動作を引き起こす。

// Lockable=boost::mutex
boost::synchronized_value<std::string> sync_s0;
{
  auto proxy0 = sync_s0.synchronize();
  sync_s0->append("abc");  // NG: 未定義動作
}

// Lockable=boost::recursive_mutex
boost::synchronized_value<std::string, boost::recursive_mutex> sync_s1;
{
  auto proxy1 = sync_s1.synchronize();
  sync_s1->append("abc");  // OK: 再帰ロック
}

トランザクション:複数オブジェクト

複数のsynchronized_value<T>オブジェクトに対する操作群を不可分実行したい場合、boost::synchronizeメンバ関数を用いて同時にプロキシオブジェクトを取り出せる(関数はstd::tuple<proxy-class>を返す)。この関数はデッドロックを引き起すことなく、指定したsynchronized_value<T>群の全ての内部ロックを一括獲得する。

boost::synchronized_value<std::string> sync_src;
boost::synchronized_value<std::string> sync_dst;

void threadN_transfer()
{
  auto proxy = boost::synchronize(sync_src, sync_dst);
  auto& src = std::get<0>(proxy);
  auto& dst = std::get<1>(proxy);

  // sync_src末尾1文字をsync_dst末尾に移動
  if (!src->empty()) {
    dst->push_back( src->back() );
    src->pop_back();
  }
}

注意:複数のsynchronized_value<T>オブジェクト操作からなるトランザクションでは、必ずboost::synchronizeメンバ関数を使用すること。プロキシオブジェクトの個別取得はデッドロックを引き起こすリスクがあるため、下記コードのような記述を行わないこと(→id:yohhoy:20120919)。

boost::synchronized_value<std::string> sync_src;
boost::synchronized_value<std::string> sync_dst;

void threadN_transactionA()
{
  // NG: src→dstの順でロック獲得
  auto src = sync_src.synchronize();
  auto dst = sync_dst.synchronize();
  //...
}

void threadN_transactionB()
{
  // NG: dst→srcの順でロック獲得
  auto dst = sync_dst.synchronize();
  auto src = sync_src.synchronize();
  //...
}

トランザクション:コピー/ムーブ代入

synchronized_value<T>オブジェクト間でのコピー/ムーブ代入操作は「2オブジェクトに対する操作」の一種であり、暗黙的に代入先および代入元オブジェクトの両内部ロック獲得が行われる(内部実装ではデッドロック回避アルゴリズムを使用)。一方、コンストラクタでは構築中オブジェクトは他スレッドから操作されないため、コピー/ムーブ元オブジェクトの内部ロック獲得のみが行われる*3

typedef boost::synchronized_value<T> sync_T_t;

sync_T_t v0, ca, ma;
// コピー/ムーブ代入操作
ca = v0;               // ca,v0 lock獲得
ma = std::move( v0 );  // ma,v0 lock獲得

sync_T_t v1;
// コピー/ムーブコンストラクタ
sync_T_t cc( v1 );             // v1 lock獲得
sync_T_t mc( std::move(v1) );  // v1 lock獲得

オブジェクト間のコピー/ムーブ操作に関する制約について、Boost.AtomicライブラリやC++11標準ライブラリ提供のatomic<T>とは異なるアプローチをとっている(→id:yohhoy:20130423)。

関連URL

*1:正確には、排他制御機構として Lockable コンセプトを満たすクラスが利用可能。template<typename T, typename Lockable = mutex> class synchronized_value;

*2:atomic<T> 変数に対する操作は、最適な機械語命令へコンパイルされると期待できる。ただし、処理系によっては atomic<T> 内部実装でミューテックスが使用される可能性はある。

*3:概念的には ”構築中オブジェクトとコピー/ムーブ元オブジェクトの両内部ロック獲得” と等価。構築中オブジェクトは自スレッドしかアクセスしないことが自明のため、クラス仕様としてはこのような定義となる。