はじめに
実はマルチスレッドでは値のインクリメントは正しく動作しない可能性があります。
// 正しく動作しない再現 const int Count = 100; var tks = new Task[Count]; var semaphore = new SemaphoreSlim(0, Count); var value = 0; // SemaphoreSlimを利用して同時アクセスのヒット率を上げている for (var i = 0; i < Count; i++) { tks[i] = Task.Run(() => { semaphore.Wait(); value++; }); } // semaphore.Waitまで辿り着くまで待つ Thread.Sleep(500); // 全てのスレッドで処理を開始する semaphore.Release(Count); // 全てのTaskが終わるまで待つ Task.WaitAll(tks); // マルチスレッドに対応していないので「99」のときが!?(タイミングによって値が変わる可能性がある) // 正しくは「100」 Console.WriteLine(value);
今回はインクリメントを筆頭にした値の増減をマルチスレッドでも扱えるようにしたInterlocked
を取り上げたいと思います。
概要
Interlocked
を用いることで複数のスレッドで利用される変数をスレッドセーフに操作することができるようになります。
Provides atomic operations for variables that are shared by multiple threads.
// DeepL翻訳
複数のスレッドで共有される変数のアトミック操作を提供する。
使い方
メソッド名 | 意味 |
---|---|
Add |
加算 |
CompareExchange |
値が等しいときのみ値を設定 |
Decrement |
デクリメント |
Exchange |
変数を設定 |
Increment |
インクリメント |
MemoryBarrier ・MemoryBarrierProcessWide |
メモリのアクセスを同期 |
Or |
ビットことのORを計算し置き換える |
Read |
読み込み(64bitシステムでは不要, 32bitシステムでは必要) |
// サンプル int x = 0; // location1(x) + value(10) Interlocked.Add(location1: ref x, value: 10); Console.WriteLine(x); // 10 // location1(x)がcomparand(10)等しい場合にはvalueに置き換える, 戻り値は置き換える前の値 int pre = Interlocked.CompareExchange(location1: ref x, value: 0, comparand: 10); Console.WriteLine(x); // 0 Console.WriteLine(pre); // 10 // location(x)-- Interlocked.Decrement(location: ref x); Console.WriteLine(x); // -1 // location1(x)をvalue(0)に置き換える Interlocked.Exchange(location1: ref x, value: 0); Console.WriteLine(x); // 0 // location(x)++ Interlocked.Increment(location: ref x); Console.WriteLine(x); // 1 // location1(x) | value(10) int pre2 = Interlocked.Or(location1: ref x, value: 10); Console.WriteLine(x); // 11 Console.WriteLine(pre2); // 1 // 32bitシステムの場合はlong, ulongをInterlocked.Readで値を読み込む long y = 10; Console.WriteLine(Interlocked.Read(ref y)); // 10