.NETは、CPUではないが.NETの基礎となるMSILにもメモリモデルがある。ECMAでのMSILの定義は、IA64も視野に入れたWeak Memory Modelになっているのだ。MSILがより強いメモリモデルを採用してしまうと、IA64で十分なパフォーマンスを出せなくなってしまうからだ。
前述の二重チェックロックのコードは、Strong Memory Modelであれば問題ないが、Weak Memory ModelのマルチプロセッサのPCでは問題になる。先に解答を示そう。解答は、利用する.NETのバージョンによって2種類ある。
using System.Threading;
public class A {
private static A _Value;
private static object ValueLock =
new object();
public static Value {
get {
if (_Value == null) {
lock (ValueLock) {
if (_Value == null) {
A temp = new A();
Thread.MemoryBarrier();
_Value = temp;
}
}
}
Return _Value;
}
}
}
ここで使ったThread.MemoryBarrierメソッドは、Visual Studio .NET 2003と共に出荷された.NET 1.1の新機能であるため、Visual Studio .NET 2002では使えない。.NET 1.0でも実行可能にする解答は、次のようになる。
public class A {
private static volatile A _Value;
private static object ValueLock =
new object();
public static Value {
get {
if (_Value == null) {
lock (ValueLock) {
if (_Value == null)
_Value = new A();
}
}
Return _Value;
}
}
}
修正前のコードと似ているが、2行目に「volatile」キーワードを入れたことに注意してほしい。
Weak Memory Modelにおいては、メモリへの書き込み順序が保障されない。このため元のコードでは、_Valueへ代入が行われた時点では_Valueオブジェクト自身への書き込みが終わっていない可能性がある。これを防ぐためには「volatile」キーワードを付加すればよいのだが、この方法を用いるとその変数へのすべてのアクセスにおいてCPU内部の最適化が禁止され、パフォーマンスの劣化を招く。
.NET 1.1で追加されたMemoryBarrier()メソッドを使えば、問題の出る可能性のある場所に限って修正することができる。このため、パフォーマンスを落とさずにWeak Memory ModelのCPUでも動作するコードへと工夫ができる。
さて、これで正しいコードとなったわけであるが、毎日書くようなコードではないだろう。そのためで、私自身でもこのコードを記憶しているわけではない。そして間違いがあってもほとんどの場合には動作してしまい、気づくのは稼働後のずっと後になってからになる。
間違えた時の確認が非常に難しいため、ほんとうに必要な場合を除いてなるべく最初に示したコードを書くようにしている、というのを理解できただろうか。
C#とVB.NETに組み込まれているlock/SyncLockは、非常に便利であるが、スレッド同期における問題は多岐に渡る。そのすべての用途に対する最適解とは言いがたい。
スレッドを使う場合には、ほかの選択肢として何があるかを理解しておき、用途に合わせて正しいものを選択するようにしたい。ここではあまり知られていないが便利なクラスを2つ紹介しておこう。
Copyright © ITmedia, Inc. All Rights Reserved.