博客 / 詳情

返回

深入理解 Interlocked.CompareExchange:C#.NET 原子操作核心原理

什麼是 Interlocked.CompareExchange?

Interlocked.CompareExchange.NETSystem.Threading.Interlocked 類的最核心原子操作方法。它執行比較並交換(Compare-And-Swap,簡稱 CAS) 操作:在多線程環境下,安全地將變量的值與預期值比較,如果相等則替換為新值,整個過程原子不可中斷。

關鍵特性

  • 原子性:整個操作在CPU級別是原子的,不會被線程調度打斷
  • 無鎖操作:無需使用鎖即可實現線程安全
  • 內存屏障:隱含完整內存屏障(full fence),確保操作前後內存訪問順序

返回值重要性:返回值是判斷操作是否成功的關鍵

  • 核心簽名(常見重載):
public static int CompareExchange(ref int location, int value, int comparand);
public static long CompareExchange(ref long location, long value, long comparand);
public static T CompareExchange<T>(ref T location, T value, T comparand) where T : class;
public static float CompareExchange(ref float location, float value, float comparand);
public static double CompareExchange(ref double location, double value, double comparand);
  • 參數解釋:

    • location:要操作的共享變量(必須用 ref 傳遞)。
    • value:如果比較成功,要寫入的新值。
    • comparand:預期舊值(用於比較)。
    • 返回值:操作前 location 的實際值(無論成功與否都返回舊值)。

CAS 是現代無鎖(lock-free)併發編程的基礎,許多高級結構(如 ConcurrentDictionarySpinLock)內部都依賴它。

為什麼使用 Interlocked.CompareExchange?

在多線程中,直接讀取-修改-寫入(如 if (count == 0) count = 1;)會產生競爭條件:多個線程可能同時讀取相同值,導致覆蓋更新。

  • 傳統鎖的問題:lock 開銷大(互斥鎖、上下文切換),不適合高頻輕量操作。
  • CAS 的優勢:

    • 無鎖(Lock-Free):不阻塞線程,高併發下性能極高。
    • 樂觀併發:假設衝突少,先嚐試操作,失敗則重試(自旋)。
    • 原子性:硬件級保證(x86 的 cmpxchg 指令)。
  • 典型應用場景:

    • 實現線程安全的單例模式(雙檢查鎖)。
    • 自定義無鎖隊列/棧。
    • 原子更新複雜狀態(標誌位 + 計數器)。
    • 實現自定義同步原語(如 SpinLockSemaphoreSlim 內部)。

傳統的 “比較 + 賦值” 是兩步非原子操作,多線程下會因競態條件導致邏輯錯誤。

// 非原子操作:多線程下可能同時通過if判斷,導致賦值錯誤
private static int _value = 0;
public static void UnsafeUpdate(int oldVal, int newVal)
{
    if (_value == oldVal) // 步驟1:讀取並比較
    {
        _value = newVal;  // 步驟2:賦值
    }
}

兩個線程可能同時通過if判斷(都讀到 _value=oldVal ),最終都執行賦值,導致邏輯錯誤。

Interlocked.CompareExchange 將 “比較” 和 “賦值” 合併為不可中斷的原子操作,從底層杜絕競態條件,且無需阻塞線程(無鎖)。

lock 的根本區別

維度 lock CompareExchange
是否阻塞
是否上下文切換
粒度 任意代碼塊 單變量
性能 較低 極高
適合 複雜邏輯 狀態切換

什麼時候該用 CompareExchange?

場景 推薦
簡單狀態切換
計數、標誌位
高併發熱點
複雜業務流程
多變量一致性

如何使用 Interlocked.CompareExchange?

最簡單的 CAS 操作(判斷是否交換成功)

private static int _counter = 0;

public static void TestCas()
{
    // 目標:如果_counter是0,就改為100
    int expected = 0;    // 預期值
    int newValue = 100;  // 新值
    
    // 執行CAS操作
    int original = Interlocked.CompareExchange(ref _counter, newValue, expected);
    
    // 判斷是否交換成功(原始值 == 預期值 → 成功)
    if (original == expected)
    {
        Console.WriteLine($"交換成功!原始值:{original},新值:{_counter}");
    }
    else
    {
        Console.WriteLine($"交換失敗!原始值:{original},當前值:{_counter}");
    }
}

輸出:交換成功!原始值:0,新值:100(若再次執行,會輸出失敗,因為_counter 已不是 0)。

原子條件更新

private int _status = 0;  // 0: 未初始化, 1: 初始化中, 2: 已完成

public void Initialize()
{
    if (Interlocked.CompareExchange(ref _status, 1, 0) == 0)
    {
        // 只有第一個線程進入這裏
        try
        {
            // 執行初始化邏輯
            DoInitialize();
        }
        finally
        {
            // 標記完成(原子操作,無需 CAS)
            Interlocked.Exchange(ref _status, 2);
        }
    }
    else
    {
        // 其他線程等待完成
        while (_status != 2) Thread.SpinWait(10);
    }
}
  • 解釋:只有當 _status 為 0 時才設置為 1 並執行初始化。

實現自旋鎖

public class SpinLock
{
    private int _lock = 0; // 0=未鎖定, 1=已鎖定
    
    public void Enter()
    {
        while (Interlocked.CompareExchange(ref _lock, 1, 0) != 0)
        {
            // 等待 - 可以使用Thread.SpinWait優化
            Thread.SpinWait(100);
        }
    }
    
    public void Exit()
    {
        Interlocked.Exchange(ref _lock, 0);
    }
}

經典場景:無鎖計數器(循環 CAS)

單次 CAS 可能因其他線程修改變量失敗,需循環重試(自旋 CAS),實現線程安全的計數器:

private static int _casCounter = 0;

/// <summary>
/// 無鎖原子遞增(替代Interlocked.Increment)
/// </summary>
public static int IncrementCounter()
{
    int current;   // 當前值
    int newValue;  // 新值
    do
    {
        // 1. 原子讀取當前值(Interlocked保證可見性)
        current = _casCounter;
        // 2. 計算新值(僅本地計算,無競態)
        newValue = current + 1;
        // 3. CAS操作:若當前值未被修改,就更新為新值
        // 返回值≠current → 被其他線程修改,重試
    } while (Interlocked.CompareExchange(ref _casCounter, newValue, current) != current);
    
    return newValue; // 返回遞增後的值
}

// 測試:1000個線程各調用1次,最終結果必為1000
var tasks = Enumerable.Range(0, 1000)
    .Select(_ => Task.Run(IncrementCounter))
    .ToList();
Task.WaitAll(tasks.ToArray());
Console.WriteLine($"最終計數器值:{_casCounter}"); // 輸出:1000

高級應用場景

泛型版本:懶加載單例(雙檢查鎖模式)

public sealed class Singleton
{
    private static Singleton _instance = null;

    private Singleton() { }

    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
            {
                var temp = new Singleton();
                Interlocked.CompareExchange(ref _instance, temp, null);
            }
            return _instance;
        }
    }
}

.NET 中需加 Lazy<T> 更安全:

private static Lazy<Singleton> _instance = new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance => _instance.Value;

無鎖狀態機(原子狀態轉換)

控制對象狀態的原子轉換(如從 Idle→Running ,避免狀態混亂):

// 定義狀態枚舉
public enum TaskState { Idle, Running, Completed, Failed }

public class TaskStateMachine
{
    private TaskState _state = TaskState.Idle;

    /// <summary>
    /// 原子轉換:Idle → Running(僅當狀態為Idle時成功)
    /// </summary>
    public bool TryStart()
    {
        TaskState original = Interlocked.CompareExchange(
            ref _state, 
            TaskState.Running,  // 新值
            TaskState.Idle      // 預期值
        );
        return original == TaskState.Idle; // 原始值=預期值 → 轉換成功
    }

    /// <summary>
    /// 原子轉換:Running → Completed
    /// </summary>
    public bool TryComplete()
    {
        TaskState original = Interlocked.CompareExchange(
            ref _state, 
            TaskState.Completed, 
            TaskState.Running
        );
        return original == TaskState.Running;
    }
}

// 使用
var stateMachine = new TaskStateMachine();
Console.WriteLine(stateMachine.TryStart());    // true(狀態變為Running)
Console.WriteLine(stateMachine.TryStart());    // false(已不是Idle)
Console.WriteLine(stateMachine.TryComplete()); // true(狀態變為Completed)

自旋實現原子加法(模擬 Interlocked.Add)

private int _counter = 0;

public int Increment()
{
    int oldValue, newValue;
    do
    {
        oldValue = _counter;
        newValue = oldValue + 1;
    } while (Interlocked.CompareExchange(ref _counter, newValue, oldValue) != oldValue);
    return newValue;
}
  • 解釋:循環直到 CAS 成功(自旋),實現無鎖遞增

對象引用替換(線程安全賦值)

private List<int> _cache = null;

public void UpdateCache(List<int> newCache)
{
    Interlocked.CompareExchange(ref _cache, newCache, _cache);  // 僅當未被其他線程修改時更新
}

Interlocked.CompareExchange 的內部實現

  • 硬件支持:

    • x86/x64LOCK CMPXCHG 指令,總線鎖保證原子性。
    • ARM:LDREX/STREX 指令對(Load-Exclusive/Store-Exclusive),失敗重試。

同時,該操作會觸發全內存屏障(Full Memory Barrier):

  • 保證操作前後的內存讀寫不會被 CPU 重排序;
  • 確保所有線程能立即看到變量的最新值(避免 CPU 緩存導致的 “髒讀”)。
  • .NET 實現(CoreCLR 簡化偽碼):
public static int CompareExchange(ref int location, int value, int comparand)
{
    // 內聯為平台特定原子指令
    fixed (int* ptr = &location)
    {
        return InterlockedCompareExchange(ptr, value, comparand);
    }
}
  • 性能:

    • 無競爭:O(1),極快。
    • 高競爭:自旋消耗 CPU,但仍遠優於鎖(無上下文切換)。

注意事項與最佳實踐

  • 自旋循環:總是用 do-while 包裝 CAS,形成“樂觀循環”。
  • ABA 問題:

    • 線程 1 讀取值為A,準備 CAS 為C;線程 2 將A改為B,又改回A;線程 1 的 CAS 會成功,但中間狀態已變化,可能導致邏輯錯誤
    • 解決方法:版本號 + 引用 CAS
  • 內存可見性:Interlocked 操作自帶全內存屏障(full fence),無需額外 volatile
  • 避免過度使用:簡單計數用 Interlocked.Increment;集合用 Concurrent 類。
  • 替代方案:

    • 高層:ConcurrentDictionary、BlockingCollection
    • 現代:System.Threading.Channels(生產者-消費者)。
    • .NET 8+:考慮 System.Threading.LockC# 12 新特性,更簡潔)。

ABA 問題

什麼是 ABA 問題?

ABA 問題 是無鎖(lock-free)編程中使用 Compare-And-Swap (CAS) 操作時的一種經典隱患。

場景描述:

  • 線程 A 讀取共享變量值:A
  • 線程 B 先將值改為 B,然後又改回 A
  • 線程 A 執行 CAS:期望值是 A,當前值也是 A → CAS 成功!
  • 但實際上值已經被其他線程修改過,語義已經改變,線程 A 卻誤以為“一切未變”。

這會導致邏輯錯誤,尤其在無鎖棧、隊列等數據結構中,可能造成節點丟失、循環引用或內存泄漏。

解決方案:版本號 + 引用 CAS

  1. 定義「版本化狀態對象」
sealed class VersionedValue
{
    public readonly int Value;
    public readonly int Version;

    public VersionedValue(int value, int version)
    {
        Value = value;
        Version = version;
    }

    public override string ToString()
        => $"Value={Value}, Version={Version}";
}

關鍵點:

  • 不可變(readonly
  • 每次修改 → 新對象
  • CAS 比較的是 對象引用
  1. 運行示例
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static VersionedValue state = new VersionedValue(1, 0);

    static void Main()
    {
        var t1 = Task.Run(Thread1);
        var t2 = Task.Run(Thread2);

        Task.WaitAll(t1, t2);

        Console.WriteLine($"Final state: {state}");
    }

    static void Thread1()
    {
        var old = state; // 讀 A(v0)
        Console.WriteLine($"T1 read: {old}");

        Thread.Sleep(200);

        var newState = new VersionedValue(3, old.Version + 1);

        var result = Interlocked.CompareExchange(
            ref state,
            newState,
            old
        );

        if (result == old)
        {
            Console.WriteLine("T1 CAS succeeded");
        }
        else
        {
            Console.WriteLine($"T1 CAS failed, current = {result}");
        }
    }

    static void Thread2()
    {
        Thread.Sleep(50);

        // A → B
        state = new VersionedValue(2, state.Version + 1);
        // B → A
        state = new VersionedValue(1, state.Version + 1);

        Console.WriteLine($"T2 changed state twice: {state}");
    }
}
  1. 典型輸出
T1 read: Value=1, Version=0
T2 changed state twice: Value=1, Version=2
T1 CAS failed, current = Value=1, Version=2
Final state: Value=1, Version=2

核心結果:

  • 值還是 1
  • 版本已經變了
  • CAS 失敗
  • ABA 被成功識別

為什麼這個方案一定能防 ABA?

CAS 比較的是“對象引用”,而不是值

即使:

Value: 1 → 2 → 1

但:

Reference: A → B → C

A ≠ C

CAS 不可能誤判成功

黃金組合

技術 作用
Interlocked.CompareExchange 原子性
不可變對象 消除中間態
版本號 明確變化歷史
GC 避免懸垂指針
.NET 官方併發集合、Immutable 系列,都是這個思想

什麼時候該用這一套?

適合:

  • 自定義狀態機
  • CAS + 狀態切換
  • 無鎖緩存
  • 高頻併發更新

不適合:

  • 普通計數
  • 簡單標誌位
  • 低併發業務

總結

Interlocked.CompareExchange 是 .NET 無鎖併發的“原子開關”
用它可以:

  • 不加鎖
  • 不阻塞
  • 在競爭中安全修改狀態
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.