「C#4 in a Nutshell」では、著者はこのクラスがMemoryBarrier
なしで0を書き込むことができることを示していますが、Core2Duoでは再現できません。
public class Foo
{
int _answer;
bool _complete;
public void A()
{
_answer = 123;
//Thread.MemoryBarrier(); // Barrier 1
_complete = true;
//Thread.MemoryBarrier(); // Barrier 2
}
public void B()
{
//Thread.MemoryBarrier(); // Barrier 3
if (_complete)
{
//Thread.MemoryBarrier(); // Barrier 4
Console.WriteLine(_answer);
}
}
}
private static void ThreadInverteOrdemComandos()
{
Foo obj = new Foo();
Task.Factory.StartNew(obj.A);
Task.Factory.StartNew(obj.B);
Thread.Sleep(10);
}
このニーズは私にはばかげているようです。これが発生する可能性のあるすべてのケースをどのように認識できますか?プロセッサが操作の順序を変更する場合、動作が変更されないことを保証する必要があると思います。
バリアを使用する気になりますか?
このバグを再現するのは非常に困難です。実際、.NET Frameworkを使用してそれを再現することは決してできないだろうと私は言っています。その理由は、Microsoftの実装では書き込みに強力なメモリモデルを使用しているためです。つまり、書き込みはあたかも揮発性であるかのように扱われます。揮発性書き込みにはロック解放のセマンティクスがあります。つまり、以前のすべての書き込みは、現在の書き込みの前にコミットする必要があります。
ただし、ECMA仕様には弱いメモリモデルがあります。したがって、Monoまたは.NET Frameworkの将来のバージョンでさえ、バグのある動作を示す可能性があることが理論的には可能です。
したがって、私が言っているのは、バリア#1と#2を削除してもプログラムの動作に影響が出る可能性はほとんどないということです。もちろん、これは保証ではありませんが、CLRの現在の実装のみに基づく観察です。
#3と#4の障壁を取り除くことは間違いなく影響があります。これは実際には非常に簡単に再現できます。まあ、この例自体ではありませんが、次のコードはよく知られているデモの1つです。 Releaseビルドを使用してコンパイルし、デバッガーの外部で実行する必要があります。バグは、プログラムが終了しないことです。 while
ループ内でThread.MemoryBarrier
を呼び出すか、stop
をvolatile
としてマークすることで、バグを修正できます。
class Program
{
static bool stop = false;
public static void Main(string[] args)
{
var t = new Thread(() =>
{
Console.WriteLine("thread begin");
bool toggle = false;
while (!stop)
{
toggle = !toggle;
}
Console.WriteLine("thread end");
});
t.Start();
Thread.Sleep(1000);
stop = true;
Console.WriteLine("stop = true");
Console.WriteLine("waiting...");
t.Join();
}
}
一部のスレッド化バグの再現が難しいのは、スレッドインターリーブのシミュレーションに使用するのと同じ方法で実際にバグを修正できるためです。 Thread.Sleep
は、メモリバリアを生成するため、最も注目すべき例です。 while
ループ内に呼び出しを配置し、バグがなくなることを確認することで、それを確認できます。
あなたが引用した本からの例の別の分析については、私の答え here を見ることができます。
オッズはvery 2番目のタスクが実行を開始するまでに最初のタスクが完了していることで十分です。この動作を観察できるのは、両方のスレッドが同時にそのコードを実行し、介在するキャッシュ同期操作がない場合のみです。コードには1つあり、StartNew()メソッドはスレッドプールマネージャー内のどこかでロックを取得します。
このコードを同時に実行する2つのスレッドを取得するのはvery困難です。このコードは数ナノ秒で完了します。何十億回も試して、オッズがあるように可変遅延を導入する必要があります。もちろん、これについてあまりポイントはありませんが、本当の問題は、あなたが期待しないを期待するときに、これがランダムに発生することです。
これを避けて、lockステートメントを使用して、正常なマルチスレッドコードを記述します。
volatile
とlock
を使用すると、メモリバリアが組み込まれます。ただし、そうでない場合は必要です。そうは言っても、あなたの例が示す数の半分が必要だと思います。
マルチスレッドのバグを再現するのは非常に困難です。通常、テストコードを何千回も実行し、バグが発生した場合にフラグを立てる自動チェックを行う必要があります。一部の行の間に短いThread.Sleep(10)を追加しようとする場合がありますが、これも常に、それがない場合と同じ問題が発生することを保証するものではありません。
メモリバリアは、マルチスレッドコードの非常にハードコアな低レベルのパフォーマンス最適化を行う必要がある人々のために導入されました。ほとんどの場合、他の同期プリミティブ(つまり、揮発性またはロック)を使用するほうがよいでしょう。
マルチスレッドに関する素晴らしい記事の1つを引用します。
次の例について考えてみます。
class Foo
{
int _answer;
bool _complete;
void A()
{
_answer = 123;
_complete = true;
}
void B()
{
if (_complete) Console.WriteLine (_answer);
}
}
メソッドAとBが異なるスレッドで同時に実行された場合、Bが「0」を書き込む可能性はありますか?答えは「はい」です。次の理由によります。
コンパイラー、CLR、またはCPUは、効率を改善するためにプログラムの命令を並べ替える場合があります。コンパイラ、CLR、またはCPUは、変数への割り当てが他のスレッドからすぐに見えないように、キャッシュの最適化を導入する場合があります。 C#とランタイムは、このような最適化によって通常のシングルスレッドコード(またはロックを適切に使用するマルチスレッドコード)が壊れないように細心の注意を払っています。これらのシナリオの外では、メモリバリア(メモリフェンスとも呼ばれる)を作成してこれらの最適化を明示的に無効にし、命令の並べ替えと読み取り/書き込みキャッシュの影響を制限する必要があります。
完全なフェンス
最も単純な種類のメモリバリアは、完全なメモリバリア(完全なフェンス)で、そのフェンスの周囲での命令の並べ替えやキャッシングを防止します。 Thread.MemoryBarrierを呼び出すと、完全なフェンスが生成されます。次のように4つの完全なフェンスを適用することで、例を修正できます。
class Foo
{
int _answer;
bool _complete;
void A()
{
_answer = 123;
Thread.MemoryBarrier(); // Barrier 1
_complete = true;
Thread.MemoryBarrier(); // Barrier 2
}
void B()
{
Thread.MemoryBarrier(); // Barrier 3
if (_complete)
{
Thread.MemoryBarrier(); // Barrier 4
Console.WriteLine (_answer);
}
}
}
Thread.MemoryBarrier
の背後にあるすべての理論と、コードを安全かつ堅牢にするために非ブロッキングシナリオでそれを使用する必要がある理由は、ここでうまく説明されています。 http://www.albahari.com/threading/part4 .aspx
2つの異なるスレッドのデータを操作している場合、これが発生する可能性があります。これは、プロセッサが速度を上げるために使用するトリックの1つです。これを行わないプロセッサを構築することはできますが、処理速度がはるかに遅くなるため、誰もそれをしません。 Hennessey and Patterson のようなものを読んで、さまざまな種類の競合状態をすべて認識する必要があります。
私は常にモニターやロックなどの高レベルのツールを使用していますが、内部的には同じようなことをしている、またはバリアを使用して実装されています。