web-dev-qa-db-ja.com

'volatile'キーワードはまだC#で​​壊れていますか?

Joe Albahariは、マルチスレッドに関する 素晴らしいシリーズ を持っています。これは必読であり、C#マルチスレッドを実行する人には心から知っておく必要があります。

ただし、パート4では、揮発性の問題について言及しています。

Volatileを適用しても、書き込みとそれに続く読み取りの交換が妨げられることはなく、これによりブレインティーザーが作成される可能性があることに注意してください。 Joe Duffyは、次の例で問題をうまく説明しています。Test1とTest2が異なるスレッドで同時に実行される場合、aとbの両方が値0になる可能性があります(xとyの両方でvolatileを使用しているにもかかわらず)

続いて、MSDNのドキュメントが正しくないことに注意してください。

MSDNのドキュメントには、volatileキーワードを使用することで、常に最新の値がフィールドに存在することが保証されると記載されています。これまで見てきたように、書き込みとそれに続く読み取りは並べ替えることができるため、これは正しくありません。

MSDNドキュメント を確認しました。これは2015年に最後に変更されましたが、まだリストされています。

Volatileキーワードは、同時に実行されている複数のスレッドによってフィールドが変更される可能性があることを示します。揮発性と宣言されたフィールドは、単一スレッドによるアクセスを想定したコンパイラー最適化の対象ではありません。 これにより、常に最新の値がフィールドに表示されます

今のところ、スレッドが古いデータを使用するのを防ぐために、より冗長なものを優先して、揮発性を避けています。

private int foo;
private object fooLock = new object();
public int Foo {
    get { lock(fooLock) return foo; }
    set { lock(fooLock) foo = value; }
}

マルチスレッドに関する部分は2011年に書かれたので、議論は今日でも有効ですか?前述のように、実行しているCPUベンダーにさえ依存する非常に困難なバグの発生を防ぐために、ロックまたはフルメモリフェンスを優先して、揮発性を回避する必要がありますか?

31
drake7707

現在の実装の揮発性は、人気のあるブログ投稿がそのようなことを主張しているにもかかわらず、壊れていません。ただし、これは正しく指定されておらず、フィールドで修飾子を使用してメモリの順序を指定するというアイデアはそれほど優れていません(Java/C#のvolatileを、以前の間違いから学ぶのに十分な時間があったC++のアトミック仕様と比較してください)。一方、MSDNの記事は、並行性について話すビジネスがなく、完全に偽物である誰かによって明確に書かれました。唯一の正しいオプションは、それを完全に無視することです。

揮発性は、フィールドにアクセスするときにセマンティクスの取得/解放を保証し、アトミック読み取りおよび書き込みを許可するタイプにのみ適用できます。 。それ以上でもそれ以下でもありません。これは、 ノンブロッキングハッシュマップ などの多くのロックフリーアルゴリズムを効率的に実装するのに十分です。

非常に単純なサンプルの1つは、揮発性変数を使用してデータを公開することです。 xの揮発性のおかげで、次のスニペットのアサーションは起動できません。

private int a;
private volatile bool x;

public void Publish()
{
    a = 1;
    x = true;
}

public void Read()
{
    if (x)
    {
        // if we observe x == true, we will always see the preceding write to a
        Debug.Assert(a == 1); 
    }
}

Volatileは使いやすくなく、ほとんどの場合、より高いレベルの概念を使用する方がはるかに良いですが、パフォーマンスが重要な場合、またはいくつかの低レベルのデータ構造を実装している場合、volatileは非常に便利です。

31
Voo

MSDNのドキュメントを読んでいると、変数にvolatileが表示された場合、コンパイラの最適化によって操作が並べ替えられるため、値が台無しになることを心配する必要はないと思います。独自のコードが別々のスレッドで間違った順序で操作を実行することによって引き起こされるエラーから保護されているとは限りません。 (確かに、コメントはこれに関して明確ではありません。)

13
Charles Bretana

volatileは非常に限定的な保証です。これは、変数が単一スレッドからのアクセスを想定するコンパイラー最適化の対象ではないことを意味します。つまり、あるスレッドから変数に書き込んだ場合、then別のスレッドから変数を読み取ると、他のスレッドは間違いなく最新の値になります。 volatileがない場合、つまりvolatileがないマルチプロセッサマシンの場合、コンパイラは、たとえば値をレジスタに保持することによって、シングルスレッドアクセスについて想定することができます。これにより、他のプロセッサが最新の値にアクセスできなくなります。

あなたが言及したコード例が示すように、それは異なるブロックのメソッドが並べ替えられることからあなたを保護しません。事実上、volatileは各個別アクセスvolatile変数アトミックにします。このようなアクセスのグループの原子性については保証しません。

プロパティに最新の単一値があることを確認したいだけの場合は、volatileを使用できるはずです。

アトミックであるかのように複数の並列操作を実行しようとすると、問題が発生します。複数の操作を強制的に一緒にアトミックにする必要がある場合は、操作全体をロックする必要があります。例をもう一度考えてみましょう。ただし、ロックを使用します。

class DoLocksReallySaveYouHere
{
  int x, y;
  object xlock = new object(), ylock = new object();

  void Test1()        // Executed on one thread
  {
    lock(xlock) {x = 1;}
    lock(ylock) {int a = y;}
    ...
  }

  void Test2()        // Executed on another thread
  {
    lock(ylock) {y = 1;}
    lock(xlock) {int b = x;}
    ...
  }
}

ロックの原因により同期が発生する可能性があり、両方abの値が0になるのを妨げる可能性があります(これはテストしていません)。ただし、xyの両方が個別にロックされているため、aまたはbのいずれかが非決定的に値0になる可能性があります。

したがって、単一の変数の変更をラップする場合は、volatileを使用しても安全であるはずであり、lockを使用しても実際には安全ではありません。複数の操作をアトミックに実行する必要がある場合は、アトミックブロック全体でlockを使用する必要があります。そうしないと、スケジューリングによって非決定的な動作が発生します。

2
zstewart