i++ 不是執行緒安全的?i++ 在 IL 層面分解為三步驟:
ldloc.0 // 載入變數值
ldc.i4.1 // 載入常數 1
add // 相加
stloc.0 // 存回變數
多執行緒並發執行時,可能發生「讀取-修改-寫入」競爭:執行緒 A 讀到 5,執行緒 B 也讀到 5,兩者各自加 1 寫回 6,預期應為 7。
| 方法 | 功能 | 回傳值 | 適用型別 |
|---|---|---|---|
Increment(ref int) | 原子遞增 1 | 遞增後的值 | int, long |
Decrement(ref int) | 原子遞減 1 | 遞減後的值 | int, long |
Add(ref int, int) | 原子加法 | 相加後的值 | int, long |
Exchange(ref T, T) | 原子交換並取得舊值 | 舊值 | 所有參考型別、int、long、float、double、IntPtr |
CompareExchange(ref T, T, T) | 若等於比較值則交換(CAS) | 原始值 | 同上 |
Read(ref long) | 原子讀取 64 位元整數 | 讀取值 | long |
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
public class AtomicDemo
{
private const int ThreadCount = 10;
private const int Iterations = 100_000;
public static void Run()
{
// 1. 非原子版
int unsafeCounter = 0;
Parallel.For(0, ThreadCount, _ =>
{
for (int i = 0; i < Iterations; i++) unsafeCounter++;
});
Console.WriteLine($"非原子: {unsafeCounter:N0} (預期 {ThreadCount * Iterations:N0})");
// 2. Interlocked 版
int safeCounter = 0;
Parallel.For(0, ThreadCount, _ =>
{
for (int i = 0; i < Iterations; i++) Interlocked.Increment(ref safeCounter);
});
Console.WriteLine($"Interlocked: {safeCounter:N0}");
// 3. lock 版(對照組)
int lockCounter = 0;
object sync = new();
Parallel.For(0, ThreadCount, _ =>
{
for (int i = 0; i < Iterations; i++)
{
lock (sync) lockCounter++;
}
});
Console.WriteLine($"lock: {lockCounter:N0}");
// 4. 效能比較
Benchmark();
}
private static void Benchmark()
{
const int N = 10_000_000;
var sw = Stopwatch.StartNew();
int c = 0;
for (int i = 0; i < N; i++) Interlocked.Increment(ref c);
sw.Stop();
Console.WriteLine($"Interlocked {N:N0} 次: {sw.ElapsedMilliseconds} ms");
sw.Restart();
c = 0;
object o = new();
for (int i = 0; i < N; i++) lock (o) c++;
sw.Stop();
Console.WriteLine($"lock {N:N0} 次: {sw.ElapsedMilliseconds} ms");
}
}
非原子: 684,321 (預期 1,000,000) // 嚴重遺失
Interlocked: 1,000,000 // 正確
lock: 1,000,000 // 正確
Interlocked 10,000,000 次: 42 ms // 約 2.4x 比 lock 快
lock 10,000,000 次: 101 ms
結論:Interlocked 以 CPU 指令級鎖定(x86 LOCK CMPXCHG、ARM LDADD)實現原子性,無核心模式切換開銷,效能遠優於 lock/Monitor。
private int _lockFlag = 0;
public void EnterSpinLock()
{
while (Interlocked.CompareExchange(ref _lockFlag, 1, 0) != 0)
{
Thread.SpinWait(10); // 避免佔滿 CPU
}
}
public void ExitSpinLock() => Interlocked.Exchange(ref _lockFlag, 0);
private object _lazyInstance;
private int _initFlag = 0;
public T GetInstance() where T : new()
{
if (Interlocked.CompareExchange(ref _initFlag, 1, 0) == 0)
{
_lazyInstance = new T();
Thread.MemoryBarrier(); // 確保寫入對其他執行緒可見
}
return (T)_lazyInstance;
}
private long _counter64;
public long Increment64() => Interlocked.Increment(ref _counter64);
// 32 位元程序下 long 非原子讀寫,Interlocked.Read/Increment 必須使用
| 誤區 | 正確觀念 |
|---|---|
以為 volatile 就能保證 i++ 安全 | volatile 僅保證可見性與防止重排,不保證原子性 |
用 Interlocked.Add(ref x, 1) 替代 Increment | 功能相同但 Increment 語意更清晰、JIT 優化更好 |
| 對 double/decimal 使用 Interlocked | 不支援,需自行用 CompareExchange + BitConverter 雙重檢查 |
| 以為 Interlocked 解決所有並發問題 | 僅適用單一變數原子操作,複雜不變條件仍需 lock/無鎖演算法 |
System.Threading.Interlocked 是 .NET 多執行緒開發的基石工具:提供硬體級原子操作、效能優於鎖、API 簡潔。掌握 Increment/CompareExchange/Exchange 三大核心,即可處理計數器、旗標、單次初始化、無鎖堆疊/佇列等高頻並發場景。記住:單一變數原子 → Interlocked;多變數/複雜邏輯 → lock 或無鎖演算法。