25

以前のいくつかの質問/回答で明確にされていますが、これはvolatileマルチスレッドではなく、C++ メモリ モデルの可視状態に関連しています。

一方、Alexandrescu によるこの記事volatileでは、このキーワードをランタイム機能としてではなく、コンパイル時のチェックとして使用して、コンパイラがスレッド セーフではない可能性のあるコードを受け入れないように強制しています。この記事では、キーワードはrequired_thread_safety、実際に意図されているvolatile.

volatileこれは適切なの(乱用)使用ですか?このアプローチには、どのような落とし穴が隠されている可能性がありますか?

最初に頭に浮かぶのは、追加の混乱です。volatileこれはスレッド セーフとは関係ありませんが、より優れたツールがないため、それを受け入れることができました。

記事の基本的な簡略化:

variable を宣言すると、その変数volatileに対してメンバー メソッドのみvolatileを呼び出すことができるため、コンパイラは他のメソッドへのコードの呼び出しをブロックします。std::vectorインスタンスを as として宣言するvolatileと、クラスのすべての使用がブロックされます。const_cast要件を解放するために実行するロッキング ポインターの形でラッパーを追加すると、ロッキング ポインターをvolatile介したすべてのアクセスが許可されます。

記事からの盗用:

template <typename T>
class LockingPtr {
public:
   // Constructors/destructors
   LockingPtr(volatile T& obj, Mutex& mtx)
      : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx)
   { mtx.Lock(); }
   ~LockingPtr()   { pMtx_->Unlock(); }
   // Pointer behavior
   T& operator*()  { return *pObj_; }
   T* operator->() { return pObj_; }
private:
   T* pObj_;
   Mutex* pMtx_;
   LockingPtr(const LockingPtr&);
   LockingPtr& operator=(const LockingPtr&);
};

class SyncBuf {
public:
   void Thread1() {
      LockingPtr<BufT> lpBuf(buffer_, mtx_);
      BufT::iterator i = lpBuf->begin();
      for (; i != lpBuf->end(); ++i) {
         // ... use *i ...
      }
   }
   void Thread2();
private:
   typedef vector<char> BufT;
   volatile BufT buffer_;
   Mutex mtx_; // controls access to buffer_
};

ノート

最初のいくつかの回答が表示された後、最も適切な言葉を使用していない可能性があるため、明確にする必要があると思います。

の使用は、volatileそれが実行時に提供するもののためではなく、コンパイル時にそれが何を意味するかによるものです。つまり、constキーワードがユーザー定義型でほとんど使用されていない場合、同じトリックをキーワードで引き出すことができますvolatile。つまり、メンバー関数呼び出しをブロックできるキーワード (たまたま volatile と綴られている) があり、Alexandrescu はそれを使用して、コンパイラをだましてスレッドセーフでないコードのコンパイルに失敗させています。

コンパイル時に何をするかという理由ではなく、コンパイラに何をさせるかという理由でそこにある多くのメタプログラミングのトリックと私は考えています。

4

8 に答える 8

6

問題は、によって提供されるスレッドセーフに関するものではないと思いますvolatile。そうではないし、Andrei の記事もそうだとは言っていない。ここでは、mutexそれを実現するために a が使用されています。問題は、静的型チェックvolatileを提供するためのキーワードの使用と、スレッドセーフなコードのためのミューテックスの使用が、キーワードの乱用であるかどうかです。私見はかなりスマートですが、厳密な型チェックのファンではない開発者に出くわしました。volatile

IMO マルチスレッド環境用のコードを書いているとき、人々が競合状態やデッドロックについて無知であることを期待することを強調するのに十分な注意がすでにあります.

このラップされたアプローチの欠点は、ラップされた型に対するすべての操作がLockingPtrメンバー関数を介して行われなければならないことです。これにより、チーム内の開発者の快適さに大きな影響を与える可能性のある間接的なレベルが 1 レベル増加します。

しかし、あなたが C++ 別名strict-type-checkingの精神を信じる純粋主義者なら、これは良い代替手段です。

于 2010-03-22T11:40:13.843 に答える
4

これは、ある種のスレッドセーフでないコード(同時アクセス)をキャッチしますが、他のコードを見逃します(ロックの反転によるデッドロック)。どちらもテストするのは特に簡単ではないので、それは適度な部分的な勝利です。実際には、特定のプライベートメンバーが特定のロックの下でのみアクセスされるという制約を強制することを覚えていることは、私にとって大きな問題ではありませんでした。

この質問に対する2つの回答は、混乱が重大な欠点であると言うのが正しいことを示しています-メンテナは、volatileのメモリアクセスセマンティクスがスレッドセーフとは何の関係もないことを理解するために非常に強く条件付けられている可能性があります。正しくないと宣言する前に、コード/記事の残りの部分を読んでください。

この記事でAlexandrescuが概説している他の大きな欠点は、クラス以外のタイプでは機能しないことだと思います。これは覚えるのが難しい制限かもしれません。データメンバーにマークを付けると、volatileロックせずにデータメンバーを使用できなくなり、コンパイラがいつロックするかを通知することを期待している場合は、誤ってint、またはテンプレートパラメータ依存型のメンバーに適用する可能性があります。結果として生じる誤ったコードは正常にコンパイルされますが、この種のエラーについてコードの検査を停止した可能性があります。に割り当てることができた場合、特にテンプレートコードで発生するエラーを想像してみてください。const intそれでも、プログラマーは、コンパイラーがそれらのconst-correctnessをチェックすることを期待していました...

volatileいつか誰かを噛むかもしれませんが、データメンバーの型が実際にメンバー関数を持っているというリスクに注意してから割り引く必要があると思います。

属性を介して追加のconstスタイルの型修飾子を提供するコンパイラーについて何か言われることがあるのだろうか。Stroustrupは、「属性を使用して、プログラムの意味に影響を与えないが、エラーの検出に役立つ可能性があるものだけを制御することをお勧めします」と述べています。volatileコード内のすべての言及をに置き換えることができれば、[[__typemodifier(needslocking)]]それはより良いと思います。その場合、オブジェクトなしでオブジェクトを使用することは不可能const_castであり、うまくいけば、const_cast破棄するものを考えずにオブジェクトを記述しないでください。

于 2010-03-22T13:35:38.180 に答える
2

C++03 §7.1.5.1p7:

volatile 修飾されていない型の左辺値を使用して、volatile 修飾された型で定義されたオブジェクトを参照しようとすると、プログラムの動作は未定義になります。

あなたの例の buffer_ は揮発性として定義されているため、それをキャストすることは未定義の動作です。ただし、オブジェクトを非揮発性として定義するが、揮発性を追加するアダプターを使用すると、これを回避できます。

template<class T>
struct Lock;

template<class T, class Mutex>
struct Volatile {
  Volatile() : _data () {}
  Volatile(T const &data) : _data (data) {}

  T        volatile& operator*()        { return _data; }
  T const  volatile& operator*() const  { return _data; }

  T        volatile* operator->()        { return &**this; }
  T const  volatile* operator->() const  { return &**this; }

private:
  T _data;
  Mutex _mutex;

  friend class Lock<T>;
};

すでにロックされているオブジェクトを介した不揮発性アクセスを厳密に制御するには、友情が必要です。

template<class T>
struct Lock {
  Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); }
  ~Lock() { _data._mutex.unlock(); }

  T& operator*() { return _data._data; }
  T* operator->() { return &**this; }

private:
  Volatile<T> &_data;
};

例:

struct Something {
  void action() volatile;  // Does action in a thread-safe way.
  void action();  // May assume only one thread has access to the object.
  int n;
};
Volatile<Something> data;
void example() {
  data->action();  // Calls volatile action.
  Lock<Something> locked (data);
  locked->action();  // Calls non-volatile action.
}

2 つの注意事項があります。まず、パブリック データ メンバー (Something::n) には引き続きアクセスできますが、修飾された揮発性になります。これはおそらくさまざまな時点で失敗します。次に、何かが本当に volatile として定義されているかどうかがわからず、メソッド内でその volatile を (「this」またはメンバーから) キャストしても、そのように定義されている場合でも UB になります。

Something volatile v;
v.action();  // Compiles, but is UB if action casts away volatile internally.

主な目標は達成されます。オブジェクトは、このように使用されていることを認識する必要がなく、コンパイラは、明示的にロックを通過しない限り、非揮発性メソッド (ほとんどの型のすべてのメソッド) の呼び出しを防止します。

于 2011-02-02T14:10:18.993 に答える
2

他のコードに基づいて構築し、volatile 指定子の必要性を完全に取り除くことで、これは機能するだけでなく、const を正しく伝達します (反復子と const_iterator と同様)。残念ながら、2 つのインターフェイス タイプに対してかなりのボイラープレート コードが必要ですが、メソッドのロジックを繰り返す必要はありません。「揮発性」バージョンを同様に「複製」する必要がある場合でも、それぞれが 1 回定義されます。 const および non-const でのメソッドの通常のオーバーロードに。

#include <cassert>
#include <iostream>

struct ExampleMutex {  // Purely for the sake of this example.
  ExampleMutex() : _locked (false) {}
  bool try_lock() {
    if (_locked) return false;
    _locked = true;
    return true;
  }
  void lock() {
    bool acquired = try_lock();
    assert(acquired);
  }
  void unlock() {
    assert(_locked);
    _locked = false;
  }
private:
  bool _locked;
};

// Customization point so these don't have to be implemented as nested types:
template<class T>
struct VolatileTraits {
  typedef typename T::VolatileInterface       Interface;
  typedef typename T::VolatileConstInterface  ConstInterface;
};

template<class T>
class Lock;
template<class T>
class ConstLock;

template<class T, class Mutex=ExampleMutex>
struct Volatile {
  typedef typename VolatileTraits<T>::Interface       Interface;
  typedef typename VolatileTraits<T>::ConstInterface  ConstInterface;

  Volatile() : _data () {}
  Volatile(T const &data) : _data (data) {}

  Interface       operator*()        { return _data; }
  ConstInterface  operator*() const  { return _data; }
  Interface       operator->()        { return _data; }
  ConstInterface  operator->() const  { return _data; }

private:
  T _data;
  mutable Mutex _mutex;

  friend class Lock<T>;
  friend class ConstLock<T>;
};

template<class T>
struct Lock {
  Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); }
  ~Lock() { _data._mutex.unlock(); }

  T& operator*() { return _data._data; }
  T* operator->() { return &**this; }

private:
  Volatile<T> &_data;
};

template<class T>
struct ConstLock {
  ConstLock(Volatile<T> const &data) : _data (data) { _data._mutex.lock(); }
  ~ConstLock() { _data._mutex.unlock(); }

  T const& operator*() { return _data._data; }
  T const* operator->() { return &**this; }

private:
  Volatile<T> const &_data;
};

struct Something {
  class VolatileConstInterface;
  struct VolatileInterface {
    // A bit of boilerplate:
    VolatileInterface(Something &x) : base (&x) {}
    VolatileInterface const* operator->() const { return this; }

    void action() const {
      base->_do("in a thread-safe way");
    }

  private:
    Something *base;

    friend class VolatileConstInterface;
  };

  struct VolatileConstInterface {
    // A bit of boilerplate:
    VolatileConstInterface(Something const &x) : base (&x) {}
    VolatileConstInterface(VolatileInterface x) : base (x.base) {}
    VolatileConstInterface const* operator->() const { return this; }

    void action() const {
      base->_do("in a thread-safe way to a const object");
    }

  private:
    Something const *base;
  };

  void action() {
    _do("knowing only one thread accesses this object");
  }

  void action() const {
    _do("knowing only one thread accesses this const object");
  }

private:
  void _do(char const *restriction) const {
    std::cout << "do action " << restriction << '\n';
  }
};

int main() {
  Volatile<Something> x;
  Volatile<Something> const c;

  x->action();
  c->action();

  {
    Lock<Something> locked (x);
    locked->action();
  }

  {
    ConstLock<Something> locked (x);  // ConstLock from non-const object
    locked->action();
  }

  {
    ConstLock<Something> locked (c);
    locked->action();
  }

  return 0;
}

class Something を Alexandrescu の volatile の使用に必要なものと比較します。

struct Something {
  void action() volatile {
    _do("in a thread-safe way");
  }

  void action() const volatile {
    _do("in a thread-safe way to a const object");
  }

  void action() {
    _do("knowing only one thread accesses this object");
  }

  void action() const {
    _do("knowing only one thread accesses this const object");
  }

private:
  void _do(char const *restriction) const volatile {
    std::cout << "do action " << restriction << '\n';
  }
};
于 2011-02-02T16:44:20.093 に答える
1

これを別の視点から見てください。変数を const として宣言すると、コードで値を変更できないことをコンパイラに伝えます。しかし、それは価値変わらないという意味ではありません。たとえば、次のようにします。

const int cv = 123;
int* that = const_cast<int*>(&cv);
*that = 42;

...これは、標準に従って未定義の動作を引き起こしますが、実際には何かが起こります。値が変更される場合があります。たぶんsigfaultがあるでしょう。フライト シミュレーターが起動するかもしれません。ポイントは、プラットフォームに依存しないベースで何が起こるかわからないということです。したがって、明らかな約束constは果たされません。値は、実際には const である場合とそうでない場合があります。

さて、これが本当だとすればconst、言葉の乱用ですか? もちろん違います。これは、より良いコードを作成するために言語が提供するツールであることには変わりありません。値が変更されないことを保証するための最終的なツールになることは決してありません - プログラマーの脳は最終的にそのツールです - しかし、それはconst役に立たないのですか?

いいえ、const をツールとして使用してより良いコードを作成することは、言語の乱用ではありません。実際、私はさらに一歩進んで、それがその機能の意図であると言います。

さて、同じことが揮発性にも当てはまります。何かを揮発性として宣言しても、プログラムはスレッドセーフになりません。おそらく、その変数またはオブジェクトをスレッドセーフにすることさえできません。しかし、コンパイラーは CV 修飾セマンティクスを強制します。慎重なプログラマーは、この事実を利用して、コンパイラーがバグを書いている可能性のある場所を特定できるようにすることで、より良いコードを作成できるようにします。彼がこれを行おうとすると、コンパイラが彼を助けるのと同じように:

const int cv = 123;
cv = 42;  // ERROR - compiler complains that the programmer is potentially making a mistake

cvの真の constnessを長い間忘れていたのと同じように、揮発性オブジェクトと変数のメモリ フェンスとアトミック性を忘れてください。ただし、言語が提供するツールを使用して、より優れたコードを記述してください。それらのツールの 1 つがvolatile.

于 2010-03-22T14:27:03.007 に答える
0

この記事では、キーワードはrequired_thread_safetyvolatile の実際の使用目的というよりもタグのように使用されています。

記事を読んでいないのに、なぜアンドレイはそのrequired_thread_safetyタグを使わないのでしょうか? ここでは、虐待volatileはあまり良い考えではないように思えます。これは、回避するのではなく、(あなたが言ったように)より多くの混乱を引き起こすと思います。

とはいえ、コンパイラが値の非同期更新に依存するチェックを最適化するのを防ぐためだけに、十分なvolatile条件でなくても、マルチスレッド コードで必要になる場合があります。

于 2011-02-02T17:55:31.027 に答える
0

そんなことはしないほうがいい。volatileは、スレッドセーフを提供するために発明されたわけではありません。メモリマップされたハードウェアレジスタに適切にアクセスするために考案されました。volatileキーワードは、CPU の順不同実行機能には影響しません。適切な OS 呼び出しまたは CPU 定義の CAS 命令、メモリ フェンスなどを使用する必要があります。

キャス

メモリーフェンス

于 2010-03-22T11:18:58.637 に答える
-2

Alexandrescu のアドバイスが正しいかどうかは特にわかりませんが、非常に頭の良い男として彼を尊敬しているにもかかわらず、彼の volatile のセマンティクスの扱いは、彼が専門分野の外に踏み出していることを示唆しています。Volatile はマルチスレッドではまったく価値がありません (この主題の適切な扱いについては、こちらを参照してください)。そのため、Alexandrescu の、Volatileマルチスレッド アクセスに役立つという主張は、彼の記事の残りの部分をどれだけ信頼できるか、真剣に考えさせられます。

于 2010-03-22T11:19:22.443 に答える