はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】インクリメントを筆頭にした値の増減をInterlockedを用いてマルチスレッドに対応する

はじめに

実はマルチスレッドでは値のインクリメントは正しく動作しない可能性があります。

// 正しく動作しない再現
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翻訳
複数のスレッドで共有される変数のアトミック操作を提供する。

Interlocked Class (System.Threading) | Microsoft Learn

使い方

メソッド名 意味
Add 加算
CompareExchange 値が等しいときのみ値を設定
Decrement デクリメント
Exchange 変数を設定
Increment インクリメント
MemoryBarrierMemoryBarrierProcessWide メモリのアクセスを同期
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