什麼是 Interlocked.CompareExchange?
Interlocked.CompareExchange 是 .NET 中 System.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)併發編程的基礎,許多高級結構(如 ConcurrentDictionary、SpinLock)內部都依賴它。
為什麼使用 Interlocked.CompareExchange?
在多線程中,直接讀取-修改-寫入(如 if (count == 0) count = 1;)會產生競爭條件:多個線程可能同時讀取相同值,導致覆蓋更新。
- 傳統鎖的問題:
lock開銷大(互斥鎖、上下文切換),不適合高頻輕量操作。 -
CAS的優勢:- 無鎖(
Lock-Free):不阻塞線程,高併發下性能極高。 - 樂觀併發:假設衝突少,先嚐試操作,失敗則重試(自旋)。
- 原子性:硬件級保證(
x86 的 cmpxchg指令)。
- 無鎖(
-
典型應用場景:
- 實現線程安全的單例模式(雙檢查鎖)。
- 自定義無鎖隊列/棧。
- 原子更新複雜狀態(標誌位 + 計數器)。
- 實現自定義同步原語(如
SpinLock、SemaphoreSlim內部)。
傳統的 “比較 + 賦值” 是兩步非原子操作,多線程下會因競態條件導致邏輯錯誤。
// 非原子操作:多線程下可能同時通過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/x64:LOCK 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。
- 線程 1 讀取值為A,準備
- 內存可見性:
Interlocked操作自帶全內存屏障(full fence),無需額外volatile。 - 避免過度使用:簡單計數用
Interlocked.Increment;集合用Concurrent類。 -
替代方案:
- 高層:
ConcurrentDictionary、BlockingCollection。 - 現代:
System.Threading.Channels(生產者-消費者)。 .NET 8+:考慮System.Threading.Lock(C# 12新特性,更簡潔)。
- 高層:
ABA 問題
什麼是 ABA 問題?
ABA 問題 是無鎖(lock-free)編程中使用 Compare-And-Swap (CAS) 操作時的一種經典隱患。
場景描述:
- 線程 A 讀取共享變量值:A
- 線程 B 先將值改為 B,然後又改回 A
- 線程 A 執行
CAS:期望值是 A,當前值也是 A →CAS成功! - 但實際上值已經被其他線程修改過,語義已經改變,線程 A 卻誤以為“一切未變”。
這會導致邏輯錯誤,尤其在無鎖棧、隊列等數據結構中,可能造成節點丟失、循環引用或內存泄漏。
解決方案:版本號 + 引用 CAS
- 定義「版本化狀態對象」
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比較的是 對象引用
- 運行示例
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}");
}
}
- 典型輸出
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 無鎖併發的“原子開關”
用它可以:
- 不加鎖
- 不阻塞
- 在競爭中安全修改狀態