Interlockedクラス

変数をスレッドセーフに更新できます。

メソッド 機能
Increment() 不可分操作として、変数を増加できる
Decrement() 不可分操作として、変数を減少できる
Add() 不可分操作として、2つの整数を加算できる
Exchange() 不可分操作として、変数を設定できる
CompareExchange() 2つの値が等しいとき、最初の値を置換できる

条件を満たしたときのみ置換できるため、たとえば初期値と比較することで初期化がくりかえされないようにできる

Read() 不可分操作として、64bit値を読み込める
MemoryBarrier() メモリのアクセスを同期できる。これはThread.MemoryBarrier()のラッパーであり、内部ではこのメソッドを呼び出すに過ぎない。MemoryBarrier - interlocked.cs

Increment()

不可分操作 (atomic operation) として、変数を増加 (インクリメント) できます。

public static int Increment (ref int location);
Increment(Int32) - Interlocked.Increment Method (System.Threading) | Microsoft Learn

locationがインクリメントされ、そのインクリメントされた値が返されます。locationはこの処理の後に他のスレッドによって書き換えられる恐れがあるため、増加させた時点での値が必要ならば、返された値を用います。

int num = 0;
Action action = () =>
{
    for (int i = 0; i < 10000; i++)
    {
        int result = Interlocked.Increment(ref num);
        // この時点で、numは他のスレッドによって書き換えられている可能性がある
    }
};

Parallel.Invoke(action, action);

この例では10000回のループを2回くり返しているため、numは最終的に20000となります。しかしnum++でインクリメントすると、そうならないことがあります。

lockからの置き換え

スレッドセーフとするにはlockを用いて、

lock (lockObject) num++;

とも記述できますが、このメソッドを用いて

Interlocked.Increment(ref num);

とした方がパフォーマンスを向上できます。一般的な推奨事項 - マネージド スレッド処理のベスト プラクティス | Microsoft Learn

Decrement()

不可分操作として、変数を減少 (デクリメント) できます。

public static int Decrement (ref int location);
Decrement(Int32) - Interlocked.Decrement Method (System.Threading) | Microsoft Learn

ConcurrentQueueでは、TryDequeue()の内部処理で用いられています。TryRemove - ConcurrentQueue.cs

Add()

不可分操作 (atomic operation) として、2つの32bit整数を加算できます。

public static int Add (
    ref int location1,
    int value
    );
Add(Int32, Int32) - Interlocked.Add Method (System.Threading) | Microsoft Learn

2つの値の合計がlocation1に格納され、それが返されます。

Exchange()

不可分操作 (atomic operation) として、変数を設定できます。

public static T Exchange<T> (
    ref T location1, // 設定する変数
    T value          // 設定する値
    ) where T : class;
Exchange<T>(T, T) - Interlocked.Exchange Method (System.Threading) | Microsoft Learn

location1valueに置き換えられ、設定する前の値が返されます。

int a1 = 1;
int a2 = Interlocked.Exchange(ref a1, 2);
// a1は2となり、1が返される

このメソッドは参照型にだけ対応します。値型にはInt32、Int64、IntPtr、SingleとDoubleに対応するオーバーロードが用意されていますが、これら以外には対応しません。Exchange<T>(T, T) - Interlocked.Exchange Method (System.Threading) | Microsoft Learn

非対応の値型は、Int32に置き換えることで対応できます。.net - How to apply InterLocked.Exchange for Enum Types in C#? - Stack Overflow

プロパティはrefで渡せない (CS0206) ため、それに設定することはできません。Errors and warnings associated with reference parameter modifiers - C# | Microsoft Learn

CompareExchange()

2つの値が等しいとき、最初の値を2番目の値に交換 (exchange) できます。比較と交換は、不可分操作として実行されます。

public static T CompareExchange<T> (
    ref T location1, // 
    T value,         // location1を置き換える値
    T comparand      // location1と比較する値
    ) where T : class;
CompareExchange<T>(T, T, T) - Interlocked.CompareExchange Method (System.Threading) | Microsoft Learn

location1comparandと等しかったならば、location1valueに置き換えられます。そして置き換える前のlocation1が返されます。

これは異なるスレッドからlocation1が同時に書き換えられるのを防ぐために用いられるメソッドであり、valueが同時に評価されることはあります。

object obj = null;
Interlocked.CompareExchange(ref obj, Init(), null);
// Init()が評価され、objはnullと等しいため、Init()の戻り値に置き換えられる

Interlocked.CompareExchange(ref obj, Init(), null);
// Init()が評価され、objはnullと等しくないため、置き換えられない

このメソッドで値型を使用しないようにします。オブジェクトは参照が等しいかで比較されるため、値型のインスタンスは同値であっても等しくないと評価され、常に交換されません。

lockからの置き換え

変数がnullのときにそれに割り当てる処理、

if (obj == null)
{
    lock (lockObject)
    {
        if (obj == null) obj = new MyClass();
    }
}

は、このメソッドを用いると次のように記述できます。一般的な推奨事項 - マネージド スレッド処理のベスト プラクティス | Microsoft Learn

if (obj == null)
{
    Interlocked.CompareExchange(ref obj, new MyClass(), null);
}

このときobjへの割り当ては1度しか行われませんが、複数のスレッドから同時にアクセスされたときに、new MyClass()が複数回呼ばれることはあります。

Read()

public static long Read (ref long location);
Read(Int64) - Interlocked.Read Method (System.Threading) | Microsoft Learn

64bitシステムでは64bitの読み取りは不可分操作のため、このメソッドは不要です。32bitではこのメソッドを用いないと、不可分操作とはなりません。

Microsoft Learnから検索