マルチスレッドで配列の要素がNULL
だったら値を入れるみたいな事がやりたかった。
アクセス頻度が高くlock
ステートメントを使うと遅くて実用に耐えないので
Interlocked.CompareExchange
でやりたいけど、大丈夫なんかいなというお話。
ソース的には雰囲気こんな感じ。
var array = new Class[N];
// (中略)
Interlocked.CompareExchange(ref array[i], new Class(), null);
結論としては「きっと大丈夫、たぶん」
まず配列の要素に対するref
はOK。
よって参照の割り当てはアトミックとされているので上記コードは理論上スレッドセーフとして成り立つはず。
ただやっぱりあまり例のない使い方で少し不安なのでInterlocked
の「分割不可能な操作」の正体について調べてみた。
ドキュメントには分割不可能な操作としか書かれていないので.NETのソースコードを追っていくと
どんどんネイティブな所に連れていかれ、最終的にはCPUレベルで実現しているという事がわかりました。
なので実装はCPUアーキテクチャによって変わりますが、x64の場合はこんな感じ。
// x64
lock cmpxchg qword ptr [mem64],rdx
念のため検証コードも回してみましたが問題なさそう。
const int N = 1000;
var array1 = new object[100000];
var array2 = new object[100000];
var counter1 = 0;
var counter2 = 0;
// 非スレッドセーフ
Parallel.For(0, N, _ =>
{
for (var i = 0; i < array1.Length; i++)
{
if (array1[i] == null)
{
array1[i] = new object();
Interlocked.Increment(ref counter1);
}
}
});
// スレッドセーフ
Parallel.For(0, N, _ =>
{
for (var i = 0; i < array2.Length; i++)
{
if (Interlocked.CompareExchange(ref array2[i], new object(), null) == null)
{
Interlocked.Increment(ref counter2);
}
}
});
Console.WriteLine($"{counter1}, {counter2}");
結果
173847, 100000
余談ですが、.NETのInterlocked回りのコードはコメントに色々書かれていて見ていて楽しいです。