C#のメモリモデル、スレッド処理
今回は、C#のマルチスレッドプログラミングと、C#の型のアトミック性についての記事です。普段、マルチスレッド処理を書くことがないので、今後使うこともあるかなと仕様について学習したことをまとめました。
C#のメモリモデルとは一連の規則で、C#のメモリモデルとは一連の規則で、 Init()で定義されている変数の並び替えが起こっても、シングルスレッドのプログラムのInitメソッドの動作は 変わりません。ところがマルチスレッドプログラムではInitがいずれか一方のフィールドのみを変更し、もう一方を 変更しない状態で、別のスレッドが_initializedフィールドと_dataフィールドを読み取ることがあり、この場合は 並び替えによってプログラムの動作が変わります。
その結果、下記コードでPrintメソッドは最終的に0を出力することになります。
public class DataInit { private int _data = 0; private bool _initialized = false; void Init() { _data = 42; // Write 1 _initialized = true; // Write 2 } void Print() { if (_initialized) // Read 1 Console.WriteLine(_data); // Read 2 else Console.WriteLine("Not initialized"); } }
また、C#では必ずしも値がメモリにアトミックに書き込まれるわけではないことも注意が必要です。あるスレッドが繰り返しSetValueを呼び出し、別のスレッドがGetValueを呼び出す場合、読み取り側のスレッドは書き込み側のあるスレッドが繰り返しSetValueを呼び出し、別のスレッドがGetValueを呼び出す場合、読み取り側のスレッドは書き込み側のスレッドが書き込んでいない値を読み取ることがあります。
例えば、 書き込み側のスレッドがGUID値(0,0,0,0)と(5,5,5,5)を指定してSetValueを交互に呼び出すと、GetValueはSetValueが代入を行っていない(0,0,0,5)、(0,0,5,5)などを読み取ることがあります。このような現象が起こる理由は代入"_value = value"がハードウェアレベルでアトミックに実行されないためです。同様に_valueの読み取りもアトミックに実行されません。
C# ECMA仕様では、アトミックに書き込まれることを保証する型として、参照型、bool,char,byte,sbyte,short,intなどを挙げています。しかし、メモリに値が正しく整列されていなかれば、これらも非アトミックに書き込み、読み込みされることに注意が必要です。
ロックの利用
ロックは多くの場合、複数のスレッドでデータを共有するもっともシンプルな方法です。ロックを適切に使えればメモリモデルの懸念事項を気にする必要はありません。
public class Test { private int _a = 0; private int _b = 0; private object _lock = new object(); void Set() { lock (_lock) { _a = 1; _b = 1; } } void Print() { lock (_lock) { int b = _b; int a = _a; Console.WriteLine("{0} {1}", a, b); } } }
上記のようにlockを付与してあげることでPrint()とSet()はアトミックに実行されるようになります。