yohhoyの日記

2013-08-03

const, mutableキーワードとスレッド安全性

C++11標準ライブラリにおいて const, mutable キーワードが持つセマンティクスと、自作クラスのスレッド安全性に関するイディオムについてメモ。本記事の内容はC++ and Beyond 2012でのHerb Sutter氏プレゼン"You Don’t Know const and mutable"に基づく。

要約:

  • C++98/03:constメンバ関数はオブジェクトに対して「logical const(論理的const)」な操作であることを意味する。constメンバ関数ではmutableメンバ変数の変更操作を含む可能性があるが*1、クラス外部から観測する限りはconst(不変操作)のように振る舞う。
  • C++11以降:C++03以前のセマンティクスに加えて、「同一オブジェクトのconstメンバ関数を複数スレッドから同時に呼び出してもよい」というスレッド安全性を表明する。標準ライブラリが提供するクラス(テンプレート)では、C++標準規格により前述のスレッド安全性が保証されている。(→id:yohhoy:20120513:p1
  • ユーザ定義クラスを設計・実装する場合にも、C++11標準ライブラリと同じスレッド安全性レベルを保証すべきである*2。この保証レベルを実現するために、クラス内部実装では「mutable修飾されたmutexメンバ変数」を利用した同期処理が必要になる。→「M&Mルール*3

C++11ではconstキーワードとmutableキーワードに、それぞれ新しいセマンティクス「スレッド安全(thread safe)」が追加される。(下表はスライドp6, p10より抜粋要約)

キーワードC++98C++11
constlogically constthread safe
(bitwise const or internally synchronized)
mutablenot observably non-constthread safe
(bitwise const or internally synchronized)

ここでの「スレッド安全」とは、C++11標準ライブラリが基本的に保障するスレッド安全性を意味する。すなわち、あるオブジェクトにおいて“constメンバ関数の呼び出し=読込操作(read)”、“非constメンバ関数の呼び出し=変更操作(modify)”として下記が成り立つ。これはintなどのプリミティブ型オブジェクトに対するデータ競合(data race)を避ける規則と等しい。

  • 異なるスレッド上における同一オブジェクトに対する操作が、全てreadアクセスであれば呼び出し側における同期処理(排他制御)は不要
  • 少なくとも1つが変更操作である場合は、操作が同時発生しないよう呼び出し側の責任で排他制御を行わなければならない

クラス利用者に対して前述のスレッド安全性を提供するために、そのクラス内部実装では次のルールを守ること。

  • constメンバ関数を「bitwise const」として実装する。つまりconstメンバ関数内ではメンバ変数のread操作しか行わない。
  • またはconstメンバ関数を「logical const」とする場合、データ競合が生じないよう下記方針で実装する。
    • mutableなメンバ変数に対する変更操作を、mutableなmutexオブジェクトを用いて排他制御する。
    • atomic変数*4などの基本的なスレッド安全性よりも強い保証を提供するクラスを用い、それらをmutableなメンバ変数として利用する。

具体例

下記コードは、多角形(Polygon)を表すクラスとキャッシュ付き面積計算の例。このPolygonクラス実装では、C++11標準ライブラリと同等の基本的なスレッド安全性を提供している。

  • Polygon::get_area()はconstメンバ関数となっているため、呼び出し側は複数スレッドから同時に同メンバ関数を呼び出すことができる(とC++11 constセマンティクスにより解釈する)。get_area()メンバ関数の内部実装では、mutableなstd::mutexオブジェクトを利用した排他制御を行っている(C++11 constセマンティクスを守るためにこう実装する義務がある)。
  • Polygon::count()もconstメンバ関数であるが、内部実装ではreadアクセス(std::vector<double>::size()=constメンバ関数の呼び出し)しか行わないため排他制御の必要がない。
  • 一方Polygon::append()は非constメンバ関数のため、呼び出し側はこのメンバ関数を他メンバ関数と同時に呼び出す事はない(とC++11 constセマンティクスにより制限される)。このため、cached_area_メンバ変数に対して排他制御なしに変更操作を行っている(これが安全であるとC++ constセマンティクスにより保証される)。
#include <vector>
#include <mutex>

class Polygon {
  std::vector<double> pts_;
  mutable std::mutex mtx_;
  mutable double cached_area_;
public:
  Polygon() : cache_area_(0.0) {}

  // 頂点追加(非constメンバ関数)
  void append(double x, double y)
  {
    pts_.append(x);  pts_.append(y);
    cached_area_ = 0.0;  // 計算済みキャッシュをクリア
  }

  // 多角形の面積を取得(constメンバ関数)
  double get_area() const
  {
    std::lock_guard<std::mutex> lk(mtx_);
    if (cached_area_ == 0.0) {
      // 面積未計算ならば計算結果をキャッシュ
      cached_area_ = calc_area();
    }
    return cached_area_;  // 計算済みキャッシュ値を返す
  }

  // 頂点数を返す(constメンバ関数)
  std::size_t count() const
  {
    return (pts_.size() / 2);
  }
private:
  double calc_area() const
  { return /* pts_から複雑な面積計算... */; }
};

関連URL

*1:constメンバ関数は「bitwise const」とは限らない。mutableメンバ変数に対する変更、つまりビット単位という物理的な観点では変更操作が行おこなわれる可能性がある。論理的constについてはプログラミング言語DのFAQの説明が詳しい(注:D言語のconstセマンティクスはC++言語とは異なる)。

*2:独自のスレッド安全性レベルを採用し、ドキュメンテーションを行うことも自由ではある。ただし、クラスライブラリ利用者からみると、C++標準ライブラリのスレッド安全性レベルよりも保証が弱いクラスライブラリは使いづらく、誤った利用によるデータ競合(data race)を引き起こすリスクが高くなる。

*3http://herbsutter.com/2013/05/24/gotw-6a-const-correctness-part-1-3/

*4:C++11標準ライブラリが提供するstd::atomic型では、atomic変数に対するいかなる操作もデータ競合を生じない。

takeuchitakeuchi 2016/02/08 10:43 本コンテンツ大変勉強になります。一点教えて下さい。
以下のようなメンバ関数は内部で排他処理せずに const 宣言
しても良いのでしょうか?

double GetX(int index) //const ??
{
int n = 2 * index;
return pts_.at(n);
}

メンバ関数の中に内部変数 n が存在しますが、
複数スレッドから同一 objectの同一メンバ関数を
同時にアクセスした場合、 n や index の値に
競合が生じるため mutex 等で保護する必要がある
と考えるべきでしょうか?

yohhoyyohhoy 2016/02/08 12:01 > takeuchiさん
該当関数GetXを const 宣言してもOKです。単なる値取得ですから、むしろ“const宣言すべき”ですね。
なお、メンバ関数内部の変数 n や index には排他制御の必要がありません。複数スレッドから呼ばれた場合でも、各メンバ関数呼び出しごとに異なる実体をもっているため、データ競合は生じません。

takeuchitakeuchi 2016/02/08 14:42 早速のご返答どうもありがとうございます。同一オブジェクトを複数からアクセスする場合、メンバ変数
のみならずメンバ関数の実体もメモリ内に単一で存在するものと思い込んでおりました。とても勉強になります。どうもありがとうございました。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/yohhoy/20130803/p1