12

複数のスレッドまたはプロセスによって書き込まれるメモリの場所から読み取るときは、以下のいくつかのケースのようにその場所にvolatileキーワードを使用する必要があることを知っていますが、コンパイラに対して実際にどのような制限があり、基本的にどのようなルールがあるかについてもっと知りたいですそのような場合に対処するときにコンパイラーは従う必要がありますか?また、メモリ位置への同時アクセスにもかかわらず、プログラマーが volatile キーワードを無視できる例外的なケースはありますか?

volatile SomeType * ptr = someAddress;
void someFunc(volatile const SomeType & input){
 //function body
}
4

5 に答える 5

19

あなたが知っていることは間違っています。Volatile は、スレッド間のメモリ アクセスの同期、あらゆる種類のメモリ フェンスの適用などには使用されません。メモリに対する操作volatileはアトミックではなく、特定の順序であるとは限りません。 volatile言語全体で最も誤解されている機能の 1 つです。" Volatile は、マルチスレッド プログラミングにはほとんど役に立ちません。 "

volatile使用されるのは、メモリにマップされたハードウェア、シグナル ハンドラー、およびsetjmpマシン コード命令とのインターフェイスです。

これは、使用されているのと同様の方法で使用することもできます。これが、この記事constで Alexandrescu が使用する方法です。しかし、間違えないでください。 コードを魔法のようにスレッドセーフにするわけではありません。この特定の方法で使用すると、これは単に、コンパイラーがどこを台無しにした可能性があるかを知らせるのに役立つツールです。間違いを修正するのはあなた次第であり、それらの間違いを修正する役割は果たしません。volatilevolatile

編集:今言ったことについて少し詳しく説明します。

変更できないものへのポインターを持つクラスがあるとします。自然にポインターを const にすることもできます。

class MyGizmo
{ 
public:
  const Foo* foo_;
};

constここであなたにとって本当に何をしますか?メモリには何もしません。古いフロッピー ディスクの書き込み禁止タブとは異なります。メモリ自体はまだ書き込み可能です。foo_ポインターを介して書き込むことはできません。これconstは、コンパイラに別の方法を提供して、混乱している可能性があることを知らせる方法にすぎません。このコードを書くとしたら:

gizmo.foo_->bar_ = 42;

...マークされているため、コンパイラはそれを許可しませんconstconst_castを使用して -ness をキャストすることでこれを回避できることは明らかですが、constこれが悪い考えであると確信する必要がある場合は、どうしようもありません。:)

Alexandrescu の の使い方volatileはまったく同じです。何らかの方法でメモリを「スレッドセーフ」にすることは何もしません。それが何をするかというと、失敗した可能性があるときにコンパイラに別の方法で知らせることです。(ミューテックスやセマフォなどの実際の同期オブジェクトを使用して) 真に「スレッドセーフ」にしたものは、 としてマークしますvolatilevolatile次に、コンパイラはそれらを非コンテキストで使用させません。コンパイラ エラーがスローされるので、考えて修正する必要があります。volatileを使用して -ness をキャストすることで再び回避できますがconst_cast、これは -ness をキャストするのと同じくらい悪ですconst

あなたへの私のアドバイスは、あなたが何をしているのか、そしてその理由を本当にvolatile理解するまで、マルチスレッドアプリケーションを書くツールとして完全に放棄することです (編集:)。ある程度の利点はありますが、ほとんどの人が考えるほどではありません。誤って使用すると、危険なほど安全でないアプリケーションを作成する可能性があります。

于 2010-11-09T18:21:42.907 に答える
10

おそらくあなたが望んでいるほど明確に定義されていません。関連する C++98 の標準のほとんどは、セクション 1.9「プログラムの実行」にあります。

抽象マシンの観察可能な動作は、データの読み取りと書き込み、volatileおよびライブラリ I/O 関数の呼び出しのシーケンスです。

左辺値 (3.10) で指定されたオブジェクトへのアクセス、オブジェクトのvolatile変更、ライブラリ I/O 関数の呼び出し、またはこれらの操作のいずれかを実行する関数の呼び出しはすべて、実行環境の状態の変化である副作用です。式の評価により、副作用が生じる場合があります。シーケンス ポイントと呼ばれる実行シーケンスの特定のポイントでは、前の評価のすべての副作用が完了し、後続の評価の副作用は発生しません。

関数の実行が開始されると、呼び出された関数の実行が完了するまで、呼び出し元の関数からの式は評価されません。

抽象マシンの処理がシグナルの受信によって中断されると、型以外のオブジェクトのvolatile sig_atomic_t値は不定にvolatile sig_atomic_tなり、ハンドラーによって変更された型以外のオブジェクトの値は不定になります。

自動保存期間 (3.7.2) を持つ各オブジェクトのインスタンスは、そのブロックへの各エントリに関連付けられています。このようなオブジェクトは存在し、ブロックの実行中、およびブロックが (関数の呼び出しまたはシグナルの受信によって) 中断されている間、最後に格納された値を保持します。

準拠する実装の最小要件は次のとおりです。

  • シーケンス ポイントでvolatileは、以前の評価が完了し、後続の評価がまだ行われていないという意味で、オブジェクトは安定しています。

  • プログラムの終了時に、ファイルに書き込まれたすべてのデータは、抽象セマンティクスに従ってプログラムを実行した場合に生成される可能性のある結果の 1 つと同一でなければなりません。

  • 対話型デバイスの入力と出力のダイナミクスは、プログラムが入力を待機する前にプロンプ​​ト メッセージが実際に表示されるような方法で行われます。対話型デバイスを構成するものは実装定義です。

つまり、要約すると次のようになります。

  • volatileコンパイラは、オブジェクトへの読み取りまたは書き込みを最適化することはできません。カサブランカが言及したような単純なケースの場合、それはあなたが考えるかもしれない方法で機能します. ただし、次のような場合には

    volatile int a;
    int b;
    b = a = 42;
    

    人々は、最後の行が読み取られたかのようにコンパイラがコードを生成する必要があるかどうかについて議論することができますし、実際に行っています

    a = 42; b = a;
    

    または、通常どおり ( がない場合volatile) を生成できる場合は、

    a = 42; b = 42;
    

    (C++0x はこの点に対処している可能性があります。私はすべてを読んでいません。)

  • コンパイラは、別々のステートメントで発生する 2 つの異なるvolatileオブジェクトに対する操作の順序を変更することはできません (すべてのセミコロンはシーケンス ポイントです)。これは、独自のスピンロックを作成しようとすべきではない多くの理由の 1 つであり、volatileマルチスレッド プログラミングの万能薬として扱わないように John Dibling が警告している主な理由です。

  • ねじについて言えば、標準テキストにねじについての言及がまったくないことに気付くでしょう。これは、C++98 にはスレッドの概念がないためです。(C++0x はそうであり、 との相互作用を十分に規定しているかもしれvolatileませんが、もし私があなただったら、まだ誰もそれらのルールを実装しているとは思いません。) したがって、あるスレッドからのオブジェクトへのアクセスが別のスレッドから可視であるという保証はありませvolatileん。スレッド。これは、volatileマルチスレッド プログラミングに特に有用ではないもう 1 つの主な理由です。

  • volatileオブジェクトが 1 つの部分でアクセスされるという保証や、オブジェクトへの変更volatileがメモリ内のオブジェクトのすぐ隣にある他のオブジェクトに触れないようにするという保証はありません。これは私が引用したものでは明示的ではありませんが、関連するものによって暗示されていますvolatile sig_atomic_t-sig_atomic_tそれ以外の場合、その部分は不要です. これによりvolatile、おそらく意図されていたよりも I/O デバイスへのアクセスの有用性が大幅に低下し、組み込みプログラミング用に販売されているコンパイラはより強力な保証を提供することがよくありますが、信頼できるものではありません。

  • 多くの人が、オブジェクトへの特定のアクセスvolatileにセマンティクスを持たせようとしています。

    T x;
    *(volatile T *)&x = foo();
    

    これは正当です (「揮発性型のオブジェクト」ではなく「揮発性左辺値で指定されたオブジェクト」と表示されているため)、細心の注意を払って行う必要があります。揮発性のものに相対的なアクセス?それが同じオブジェクトであっても(とにかく私が知る限り)そうです。

  • 複数の volatile 値へのアクセスの並べ替えが心配な場合は、シーケンス ポイントの規則を理解する必要があります。これは長くて複雑です。この回答はすでに長すぎるため、ここでは引用しませんが、少しだけ簡略化された良い説明。C と C++ の間のシーケンス ポイント規則の違いについて心配する必要がある場合は、既にどこかで失敗しています (たとえば、経験則として、オーバーロードしないで&&ください)。

于 2010-11-09T18:25:23.557 に答える
7

除外される特定の非常に一般的な最適化はvolatile、値をメモリからレジスタにキャッシュし、繰り返しアクセスするためにレジスタを使用することです (これは、毎回メモリに戻るよりもはるかに高速であるためです)。

代わりに、コンパイラは毎回メモリから値をフェッチする必要があります (Zach からヒントを得て、「毎回」はシーケンス ポイントによって制限されていると言うべきです)。

また、一連の書き込みでレジスタを使用して、後で最終値を書き戻すこともできません。すべての書き込みをメモリにプッシュする必要があります。

なぜこれが役立つのですか?一部のアーキテクチャでは、特定の IO デバイスがその入力または出力をメモリ位置にマップします (つまり、その位置に書き込まれたバイトが実際にシリアル ラインに出力されます)。コンパイラがこれらの書き込みの一部を、たまにしかフラッシュされないレジスタにリダイレクトすると、ほとんどのバイトはシリアルラインに送られません。良くない。を使用するとvolatile、この状況を防ぐことができます。

于 2010-11-09T18:31:21.210 に答える
7

変数を として宣言するvolatileことは、コンパイラが値について、他の方法で行うことができた可能性のある仮定を行うことができないことを意味するため、コンパイラがさまざまな最適化を適用することを防ぎます。基本的に、コードの通常のフローで値が変更されない場合でも、アクセスごとにコンパイラーがメモリから値を再読み取りするように強制します。例えば:

int *i = ...;
cout << *i; // line A
// ... (some code that doesn't use i)
cout << *i; // line B

この場合、コンパイラは通常、 の値iが途中で変更されていないため、行 A の値を保持し (たとえばレジスタに)、同じ値を B に出力しても問題ないと想定しますi。 、外部ソースが行AとBの間volatileの値を変更した可能性があることをコンパイラーに伝えているため、コンパイラーはメモリから現在の値を再フェッチする必要があります。i

于 2010-11-09T18:13:15.757 に答える
1

コンパイラは、ループ内の揮発性オブジェクトの読み取りを最適化することはできません。それ以外の場合は通常実行します (つまり、strlen())。

これは、固定アドレスでハードウェア レジストリを読み取るときに組み込みプログラミングで一般的に使用され、その値は予期せず変更される可能性があります。(「通常の」メモリとは対照的に、それはプログラム自体によって書き込まれない限り変更されません...)

それが主な目的です。

また、あるスレッドが別のスレッドによって書き込まれた値の変更を確認できるようにするためにも使用できますが、そのオブジェクトの読み取り/書き込み時の原子性を保証するものではありません。

于 2010-11-09T18:13:56.570 に答える