.NET Lock
.NET lock 语句实现原理详解
1. 概述
.NET 的 lock 语句是基于 Monitor 类实现的线程同步机制。它采用了 混合锁(Hybrid Lock) 的设计理念,结合了轻量级锁(Thin Lock)和重量级锁(Fat Lock),以在不同场景下提供最佳性能。
1.1 设计目标
- 低开销:无竞争时几乎零开销
- 可重入:支持同一线程多次获取同一锁
- 公平性:避免线程饥饿,保证等待线程最终能获取锁
- 异常安全:确保在异常情况下能正确释放锁
2. 核心架构
2.1 对象头结构(ObjHeader)
每个 .NET 对象都有一个对象头,用于存储同步信息:
class ObjHeader
{
private:
#ifdef HOST_64BIT
DWORD m_alignpad; // 64位对齐填充
#endif
Volatile<DWORD> m_SyncBlockValue; // 同步块值(包含索引和标志位)
};
2.2 SyncBlockValue 位字段布局
// 位掩码定义
#define BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX 0x08000000 // 是否为哈希码或同步块索引
#define BIT_SBLK_IS_HASHCODE 0x04000000 // 是否为哈希码
#define BIT_SBLK_SPIN_LOCK 0x10000000 // 自旋锁标志
// Thin Lock 布局(当 BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX 为 0)
#define SBLK_MASK_LOCK_THREADID 0x0000FFFF // 线程ID (bits 0-15)
#define SBLK_MASK_LOCK_RECLEVEL 0x003F0000 // 递归级别 (bits 16-21)
#define SBLK_LOCK_RECLEVEL_INC 0x00010000 // 递归级别增量
位字段布局图:
31 28 26 25 22 21 16 15 0
|-----|--|--|----|---|-------|---|-------------|
|flags|sp|hb|hash| - |reclev |tid| threadid |
- flags: 预留标志位
- sp: 自旋锁位
- hb: 哈希或同步块索引标志
- hash: 哈希码标志
- reclev: 递归级别(0-63)
- threadid: 持有锁的线程ID(0-65535)
3. 三级锁机制
3.1 Thin Lock(轻量级锁)
特点:
- 直接存储在对象头中
- 支持 65535 个不同的线程ID
- 支持最多 63 级递归
- 无竞争时性能最佳
获取过程:
bool TryEnter_FastPath(object obj)
{
DWORD oldValue = obj->GetHeader()->m_SyncBlockValue;
// 检查是否为 thin lock 且未被持有
if ((oldValue & (BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | SBLK_MASK_LOCK_THREADID)) == 0)
{
DWORD threadId = GetCurrentThreadId() & SBLK_MASK_LOCK_THREADID;
DWORD newValue = oldValue | threadId;
// 原子操作尝试获取锁
if (InterlockedCompareExchange(&obj->GetHeader()->m_SyncBlockValue, newValue, oldValue) == oldValue)
{
return true; // 成功获取
}
}
return false; // 需要走慢路径
}
递归处理:
// 检查是否为同一线程的递归调用
DWORD currentThreadId = GetCurrentThreadId() & SBLK_MASK_LOCK_THREADID;
if ((oldValue & SBLK_MASK_LOCK_THREADID) == currentThreadId)
{
// 检查递归级别是否超限
if ((oldValue & SBLK_MASK_LOCK_RECLEVEL) < SBLK_MASK_LOCK_RECLEVEL)
{
DWORD newValue = oldValue + SBLK_LOCK_RECLEVEL_INC;
if (InterlockedCompareExchange(&m_SyncBlockValue, newValue, oldValue) == oldValue)
{
return true; // 递归成功
}
}
}
3.2 Fat Lock(重量级锁)
当发生以下情况时,会升级为 Fat Lock:
- 发生锁竞争
- 递归级别超过 63
- 线程ID超过 65535
- 调用
Wait/Pulse方法
SyncBlock 结构:
class SyncBlock
{
AwareLock m_Monitor; // 实际的锁对象
SLink m_Link; // 用于线程等待队列
DWORD m_dwHashCode; // 对象哈希码
// ... 其他字段
};
class AwareLock
{
private:
LockState m_lockState; // 锁状态(原子操作)
ULONG m_Recursion; // 递归计数
DWORD m_HoldingThreadId; // 持有线程ID
SIZE_T m_HoldingOSThreadId; // 操作系统线程ID
CLREvent m_SemEvent; // 等待事件
};
3.3 锁升级过程
void UpgradeToFatLock(Object* obj)
{
// 1. 分配 SyncBlock
SyncBlock* syncBlock = SyncBlockCache::GetSyncBlock();
// 2. 从 thin lock 状态迁移信息
DWORD oldValue = obj->GetHeader()->m_SyncBlockValue;
if (!(oldValue & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX))
{
DWORD threadId = oldValue & SBLK_MASK_LOCK_THREADID;
DWORD recLevel = (oldValue & SBLK_MASK_LOCK_RECLEVEL) >> SBLK_RECLEVEL_SHIFT;
if (threadId != 0) // 如果锁被持有
{
// 初始化 fat lock 为已持有状态
syncBlock->m_Monitor.InitializeToLockedWithNoWaiters(
recLevel + 1, threadId, GetOSThreadId(threadId));
}
}
// 3. 原子地设置同步块索引
DWORD syncBlockIndex = SyncBlockCache::GetIndex(syncBlock);
obj->GetHeader()->SetIndex(syncBlockIndex | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX);
}
4. Monitor 类实现
4.1 Enter 方法
public static void Enter(object obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
// 快路径:尝试获取 thin lock
if (!TryEnter_FastPath(obj))
{
// 慢路径:处理竞争情况
Enter_Slowpath(obj);
}
}
快路径实现:
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool TryEnter_FastPath(object obj);
// Native 实现
bool TryEnter_FastPath(Object* obj)
{
ObjHeader* objHeader = obj->GetHeader();
for (int attempt = 0; attempt < 10; ++attempt)
{
DWORD oldValue = objHeader->m_SyncBlockValue.LoadWithoutBarrier();
// 检查是否可以直接获取 thin lock
if (CanAcquireThinLock(oldValue))
{
DWORD newValue = CalculateNewThinLockValue(oldValue);
if (objHeader->m_SyncBlockValue.CompareExchange(newValue, oldValue) == oldValue)
{
return true;
}
// CAS 失败,重试
continue;
}
// 无法获取 thin lock
break;
}
return false;
}
4.2 Exit 方法
public static void Exit(object obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
LeaveHelperAction action = Exit_FastPath(obj);
if (action != LeaveHelperAction.None)
{
Exit_Slowpath(action, obj);
}
}
快路径释放:
LeaveHelperAction Exit_FastPath(Object* obj)
{
ObjHeader* objHeader = obj->GetHeader();
DWORD oldValue = objHeader->m_SyncBlockValue.LoadWithoutBarrier();
// 检查是否为 thin lock
if (!(oldValue & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX))
{
DWORD threadId = GetCurrentThreadId() & SBLK_MASK_LOCK_THREADID;
// 验证当前线程是否持有锁
if ((oldValue & SBLK_MASK_LOCK_THREADID) == threadId)
{
DWORD recLevel = oldValue & SBLK_MASK_LOCK_RECLEVEL;
if (recLevel == 0)
{
// 完全释放锁
DWORD newValue = oldValue & ~SBLK_MASK_LOCK_THREADID;
if (objHeader->m_SyncBlockValue.CompareExchange(newValue, oldValue) == oldValue)
{
return LeaveHelperAction.None; // 成功释放
}
}
else
{
// 减少递归级别
DWORD newValue = oldValue - SBLK_LOCK_RECLEVEL_INC;
if (objHeader->m_SyncBlockValue.CompareExchange(newValue, oldValue) == oldValue)
{
return LeaveHelperAction.None; // 成功递减
}
}
}
else
{
return LeaveHelperAction.Error; // 不是持有者
}
}
return LeaveHelperAction.UseSlowPath; // 需要慢路径处理
}
5. AwareLock 实现
5.1 锁状态管理
class LockState
{
private:
static const UINT32 IsLockedMask = 1 << 0; // 是否被锁定
static const UINT32 ShouldNotPreemptWaitersMask = 1 << 1; // 是否应避免抢占等待者
static const UINT32 SpinnerCountMask = 0x7 << 2; // 自旋线程计数
static const UINT32 IsWaiterSignaledToWakeMask = 1 << 5; // 是否有等待者被信号唤醒
static const UINT32 WaiterCountMask = ~0x3F; // 等待者计数
private:
UINT32 m_state;
public:
bool IsLocked() const { return !!(m_state & IsLockedMask); }
bool HasAnyWaiters() const { return m_state >= WaiterCountIncrement; }
bool HasAnySpinners() const { return !!(m_state & SpinnerCountMask); }
};
5.2 自旋等待策略
void AwareLock::SpinWait(const YieldProcessorNormalizationInfo &normalizationInfo, DWORD spinIteration)
{
// 自适应自旋策略
if (spinIteration < g_SpinConstants.dwInitialDuration)
{
// 短暂自旋,不让出CPU
YieldProcessor();
}
else if (spinIteration < g_SpinConstants.dwMaximumDuration)
{
// 中等自旋,偶尔让出CPU
if ((spinIteration % 8) == 0)
{
__SwitchToThread();
}
else
{
YieldProcessor();
}
}
else
{
// 长时间等待,完全让出CPU
__SwitchToThread();
}
}
5.3 等待队列管理
struct ThreadQueue
{
// 将线程加入等待队列
static void EnqueueThread(WaitEventLink *pWaitEventLink, SyncBlock *psb)
{
// 遍历到队列末尾(FIFO 策略)
SLink *pPrior = &psb->m_Link;
while (pPrior->m_pNext)
{
pPrior = pPrior->m_pNext;
}
// 将新等待者添加到末尾
pPrior->m_pNext = &pWaitEventLink->m_LinkSB;
pWaitEventLink->m_LinkSB.m_pNext = NULL;
}
// 从队列中移除头部线程
static WaitEventLink *DequeueThread(SyncBlock *psb)
{
SLink *pLink = psb->m_Link.m_pNext;
if (pLink)
{
psb->m_Link.m_pNext = pLink->m_pNext;
return WaitEventLinkForLink(pLink);
}
return NULL;
}
};
6. lock 语句的编译器转换
6.1 C# 代码
lock (obj)
{
// 临界区代码
DoSomething();
}
6.2 编译器生成的等价代码
object lockObj = obj;
bool lockTaken = false;
try
{
Monitor.Enter(lockObj, ref lockTaken);
// 临界区代码
DoSomething();
}
finally
{
if (lockTaken)
{
Monitor.Exit(lockObj);
}
}
6.3 IL 代码分析
// lock 进入
ldloc.0 // 加载 obj
dup // 复制引用
stloc.1 // 存储到 lockObj
ldloca.s 2 // 加载 lockTaken 地址
call Monitor.Enter(object, bool&)
// 临界区代码
// ... 用户代码 ...
// lock 退出(在 finally 块中)
ldloc.2 // 加载 lockTaken
brfalse.s EXIT
ldloc.1 // 加载 lockObj
call Monitor.Exit(object)
EXIT:
7. 性能优化技术
7.1 锁消除(Lock Elision)
JIT 编译器可以在某些情况下完全消除锁:
// 这个锁可能被消除,因为 obj 是局部对象
var obj = new object();
lock (obj)
{
// 只有当前线程可以访问 obj
localVariable++;
}
7.2 锁粗化(Lock Coarsening)
// 优化前:多个细粒度锁
lock (obj) { DoA(); }
lock (obj) { DoB(); }
lock (obj) { DoC(); }
// 优化后:单个粗粒度锁
lock (obj)
{
DoA();
DoB();
DoC();
}
7.3 偏向锁(Biased Locking)
在单线程场景下,thin lock 的获取和释放可以进一步优化:
// 记录最后获取锁的线程
DWORD lastLockingThread = obj->GetHeader()->GetLastLockingThread();
if (lastLockingThread == currentThreadId)
{
// 快速递归获取,无需 CAS 操作
obj->GetHeader()->IncrementRecursionLevel();
return true;
}
8. 内存模型保证
8.1 获取-释放语义
// Monitor.Enter 提供获取语义
void Monitor::Enter()
{
// ... 锁获取逻辑 ...
// 确保临界区内的内存操作不会重排序到锁获取之前
MemoryBarrier();
}
// Monitor.Exit 提供释放语义
void Monitor::Exit()
{
// 确保临界区内的内存操作不会重排序到锁释放之后
MemoryBarrier();
// ... 锁释放逻辑 ...
}
8.2 volatile 语义
对象头的 m_SyncBlockValue 字段使用 Volatile<DWORD> 确保:
- 读操作具有获取语义
- 写操作具有释放语义
- 防止编译器和CPU的重排序
8.3 并发编程核心概念详解
8.3.1 什么是临界区(Critical Section)
临界区是指访问共享资源的代码段,在任意时刻只能被一个线程执行。临界区是并发控制的核心概念。
临界区的特性
class BankAccount
{
private decimal _balance = 1000m;
private readonly object _lock = new object();
// 临界区:访问共享资源 _balance 的代码段
public void Withdraw(decimal amount)
{
lock (_lock) // 临界区开始
{
// 这整个代码块就是临界区
if (_balance >= amount)
{
Console.WriteLine($"当前余额: {_balance}");
Thread.Sleep(100); // 模拟处理时间
_balance -= amount;
Console.WriteLine($"提取 {amount},剩余余额: {_balance}");
}
else
{
Console.WriteLine("余额不足");
}
} // 临界区结束
}
}
为什么需要临界区保护
// 没有临界区保护的危险代码
public void UnsafeWithdraw(decimal amount)
{
// 竞态条件示例
if (_balance >= amount) // 线程1和线程2同时检查,都看到余额1000
{
Thread.Sleep(100); // 模拟网络延迟
_balance -= amount; // 线程1: 1000-800=200, 线程2: 1000-600=400
} // 结果:最后执行的线程覆盖了前一个线程的结果
}
// 可能的执行序列:
// 时间点1: 线程1检查余额1000 >= 800 ✓
// 时间点2: 线程2检查余额1000 >= 600 ✓
// 时间点3: 线程1设置余额 = 200
// 时间点4: 线程2设置余额 = 400 (错误!覆盖了线程1的结果)
临界区的设计原则
class OptimizedBankAccount
{
private decimal _balance = 1000m;
private readonly object _lock = new object();
public bool Withdraw(decimal amount)
{
// 原则1: 最小化临界区 - 预先准备数据
bool canWithdraw;
decimal newBalance;
lock (_lock)
{
// 原则2: 临界区内只做必要的原子操作
canWithdraw = _balance >= amount;
if (canWithdraw)
{
newBalance = _balance - amount;
_balance = newBalance;
}
else
{
newBalance = _balance;
}
}
// 原则3: 日志和通知放在临界区外
if (canWithdraw)
{
LogTransaction($"提取 {amount},剩余 {newBalance}");
NotifyBalanceChanged(newBalance);
}
else
{
LogError($"提取失败,余额不足 {newBalance}");
}
return canWithdraw;
}
}
8.3.2 volatile 与内存可见性
内存可见性指的是一个线程对共享变量的修改,能否及时被其他线程看到。
CPU 缓存导致的可见性问题
CPU架构下的内存层次:
CPU1 CPU2
| |
+----------+ +----------+
| L1 Cache | | L1 Cache | <- 每个CPU核心私有
+----------+ +----------+
| |
+----------+ +----------+
| L2 Cache | | L2 Cache | <- 可能共享或私有
+----------+ +----------+
| |
+------------------------+
| L3 Cache | <- 多核心共享
+------------------------+
|
+------------------------+
| 主内存(RAM) | <- 所有CPU共享
+------------------------+
volatile 的作用机制
class VolatileExample
{
// 没有 volatile 的变量
private bool _stopRequested = false;
// 使用 volatile 的变量
private volatile bool _stopRequestedVolatile = false;
// 问题场景:编译器/CPU优化导致的问题
public void WorkerThread()
{
while (!_stopRequested) // 可能被优化为 while(true)
{
DoWork();
// 编译器可能认为 _stopRequested 在循环中不会改变
// 因此优化为只读取一次,缓存在寄存器中
}
}
// 解决方案:使用 volatile
public void VolatileWorkerThread()
{
while (!_stopRequestedVolatile) // 每次都从内存读取
{
DoWork();
// volatile 确保每次都从主内存读取最新值
}
}
public void Stop()
{
_stopRequested = true; // 可能只写入CPU缓存
_stopRequestedVolatile = true; // 强制写入主内存
}
}
volatile 在 .NET 中的实现
// .NET 中的 volatile 实现
public class VolatileOperations
{
private int _counter;
private volatile int _volatileCounter;
// 普通读写
public void NormalOperation()
{
_counter = 42; // 可能被重排序
int value = _counter; // 可能读取缓存值
}
// volatile 读写
public void VolatileOperation()
{
_volatileCounter = 42; // 立即刷新到主内存
int value = _volatileCounter; // 从主内存读取
}
// 等价的 Volatile 类方法
public void ExplicitVolatileOperation()
{
Volatile.Write(ref _counter, 42); // 显式 volatile 写
int value = Volatile.Read(ref _counter); // 显式 volatile 读
}
}
volatile 的语义保证
class VolatileSemantics
{
private volatile bool _flag;
private int _data;
// 写线程
public void Writer()
{
_data = 42; // 1. 设置数据
_flag = true; // 2. 设置标志 (volatile写)
// volatile 写保证:
// - 所有在 volatile 写之前的操作都不会被重排序到之后
// - volatile 写会立即刷新到主内存
}
// 读线程
public void Reader()
{
if (_flag) // 1. 检查标志 (volatile读)
{
int value = _data; // 2. 读取数据
Console.WriteLine($"读取到数据: {value}");
}
// volatile 读保证:
// - 所有在 volatile 读之后的操作都不会被重排序到之前
// - volatile 读会从主内存获取最新值
// - 如果看到 _flag = true,一定能看到 _data = 42
}
}
8.3.3 内存操作重排序
内存操作重排序是编译器和CPU为了提高性能而改变指令执行顺序的优化技术。
重排序的类型
class ReorderingExample
{
private int _a, _b;
private bool _flag1, _flag2;
// 编译器重排序示例
public void CompilerReordering()
{
// 源代码顺序
_a = 1; // 操作1
_b = 2; // 操作2
_flag1 = true; // 操作3
// 编译器可能重排序为:
// _b = 2; (操作2先执行,因为不依赖_a)
// _a = 1;
// _flag1 = true;
}
// CPU重排序示例
public void CpuReordering()
{
// 即使编译器不重排序,CPU也可能重排序:
_a = 1; // 可能在Store Buffer中等待
_flag1 = true; // 可能先于_a写入内存
// 其他CPU可能看到:_flag1 = true 但 _a 还是旧值
}
}
重排序导致的问题
class ReorderingProblem
{
private int _data;
private volatile bool _ready;
// 生产者线程
public void Producer()
{
_data = 42; // 1. 设置数据
_ready = true; // 2. 设置就绪标志
// 问题:如果发生重排序,可能变成:
// _ready = true; (先设置标志)
// _data = 42; (后设置数据)
// 这会导致消费者看到 ready=true 但 data 还是旧值
}
// 消费者线程
public void Consumer()
{
while (!_ready) // 等待数据就绪
{
Thread.Yield();
}
Console.WriteLine($"数据: {_data}"); // 可能读到旧值!
}
}
内存屏障(Memory Barrier)
class MemoryBarrierExample
{
private int _data;
private bool _ready;
// 使用内存屏障防止重排序
public void ProducerWithBarrier()
{
_data = 42; // 1. 设置数据
Thread.MemoryBarrier(); // 内存屏障:确保上面的写操作完成
_ready = true; // 2. 设置标志
// 内存屏障确保:
// - 屏障前的所有内存操作都在屏障后的操作之前完成
// - 防止编译器和CPU重排序
}
public void ConsumerWithBarrier()
{
while (!_ready)
{
Thread.Yield();
}
Thread.MemoryBarrier(); // 确保读取到最新的数据
Console.WriteLine($"数据: {_data}");
}
}
.NET 中的内存屏障类型
public static class MemoryBarriers
{
// 完整内存屏障
public static void FullBarrier()
{
Thread.MemoryBarrier(); // 阻止所有重排序
}
// 获取屏障(LoadLoad + LoadStore)
public static void AcquireBarrier()
{
// 阻止读操作与后续读/写操作重排序
// 相当于 volatile 读的语义
Volatile.Read(ref _dummy);
}
// 释放屏障(LoadStore + StoreStore)
public static void ReleaseBarrier()
{
// 阻止前面的读/写操作与后续写操作重排序
// 相当于 volatile 写的语义
Volatile.Write(ref _dummy, 0);
}
private static int _dummy;
}
实际应用:双重检查锁定
class Singleton
{
private static volatile Singleton _instance;
private static readonly object _lock = new object();
public static Singleton Instance
{
get
{
// 第一次检查(避免不必要的锁)
if (_instance == null)
{
lock (_lock)
{
// 第二次检查(确保线程安全)
if (_instance == null)
{
// 创建实例的操作可能被重排序:
// 1. 分配内存
// 2. 调用构造函数
// 3. 赋值给_instance
// 如果没有volatile,可能重排序为:
// 1. 分配内存
// 2. 赋值给_instance (未初始化的对象!)
// 3. 调用构造函数
_instance = new Singleton(); // volatile确保正确顺序
}
}
}
return _instance;
}
}
private Singleton() { /* 初始化代码 */ }
}
高级示例:无锁数据结构
class LockFreeStack<T>
{
private volatile Node _head;
private class Node
{
public T Data;
public volatile Node Next;
public Node(T data) { Data = data; }
}
public void Push(T item)
{
var newNode = new Node(item);
while (true)
{
Node currentHead = _head; // volatile 读
newNode.Next = currentHead; // 设置新节点的下一个节点
// 关键:CAS操作具有完整的内存屏障语义
if (Interlocked.CompareExchange(ref _head, newNode, currentHead) == currentHead)
{
break; // 成功更新头节点
}
// 失败则重试(其他线程修改了头节点)
}
}
public bool TryPop(out T result)
{
while (true)
{
Node currentHead = _head; // volatile 读
if (currentHead == null)
{
result = default(T);
return false;
}
Node newHead = currentHead.Next; // 获取新的头节点
// CAS操作确保原子性和内存可见性
if (Interlocked.CompareExchange(ref _head, newHead, currentHead) == currentHead)
{
result = currentHead.Data;
return true;
}
// 失败则重试
}
}
}
8.3.4 总结与最佳实践
概念关系图
并发控制层次结构:
应用层面:
┌─────────────────┐
│ 临界区保护 │ <- lock, Monitor, Mutex等
│ (Critical │
│ Section) │
└─────────────────┘
↓
内存模型层面:
┌─────────────────┐
│ 获取-释放语义 │ <- volatile, Interlocked等
│ (Acquire- │
│ Release) │
└─────────────────┘
↓
硬件层面:
┌─────────────────┐
│ 内存屏障 │ <- CPU指令,防止重排序
│ (Memory │
│ Barrier) │
└─────────────────┘
选择合适的同步机制
class SynchronizationGuidelines
{
// 场景1: 保护复杂的临界区
private readonly object _lock = new object();
private int _complexState1, _complexState2;
public void UpdateComplexState()
{
lock (_lock) // 推荐:使用lock保护复杂操作
{
_complexState1 = CalculateNewValue1();
_complexState2 = CalculateNewValue2();
ValidateConsistency();
}
}
// 场景2: 简单的标志变量
private volatile bool _stopRequested;
public void RequestStop()
{
_stopRequested = true; // 推荐:volatile用于简单标志
}
// 场景3: 原子计数器
private int _counter;
public void IncrementCounter()
{
Interlocked.Increment(ref _counter); // 推荐:原子操作
}
// 场景4: 单次初始化
private static readonly Lazy<Resource> _resource =
new Lazy<Resource>(() => new Resource()); // 推荐:Lazy<T>
public static Resource GetResource()
{
return _resource.Value;
}
}
性能考虑
// 性能对比(大致数量级)
class PerformanceComparison
{
// 最快:无同步
public void NoSync() { } // ~1 纳秒
// 很快:volatile 读写
private volatile int _vol;
public void VolatileOp() { _vol = 42; } // ~2-5 纳秒
// 快:原子操作
private int _atomic;
public void AtomicOp() { Interlocked.Increment(ref _atomic); } // ~10-50 纳秒
// 中等:无竞争的lock
private readonly object _lock = new object();
public void LockOp() { lock(_lock) { } } // ~50-200 纳秒
// 慢:有竞争的lock
public void ContentedLock() { /* 竞争情况 */ } // ~1-100 微秒
// 很慢:内核对象(Mutex等)
public void KernelSync() { /* Mutex操作 */ } // ~1-10 毫秒
}
通过理解这些核心概念,您可以更好地设计和调试多线程程序,选择合适的同步机制来保证程序的正确性和性能。
9. 性能特征
9.1 时间复杂度
| 场景 | Thin Lock | Fat Lock |
|---|---|---|
| 无竞争获取 | O(1) | O(1) |
| 无竞争释放 | O(1) | O(1) |
| 竞争获取 | 升级到 Fat Lock | O(1) + 等待时间 |
| 递归获取 | O(1) | O(1) |
9.2 内存开销
| 锁类型 | 内存开销 | 说明 |
|---|---|---|
| Thin Lock | 0 字节 | 直接存储在对象头中 |
| Fat Lock | ~200 字节 | SyncBlock + AwareLock + 相关结构 |
9.3 性能基准
无竞争场景(典型):
- Thin Lock: ~1-2 CPU 周期
- Fat Lock: ~10-20 CPU 周期
竞争场景:
- 自旋等待: ~100-1000 CPU 周期
- 内核等待: ~10,000+ CPU 周期
10. 最佳实践
10.1 锁对象选择
// ✅ 推荐:使用专门的锁对象
private readonly object _lock = new object();
public void Method()
{
lock (_lock)
{
// 临界区代码
}
}
// ❌ 不推荐:锁定字符串常量
lock ("string literal") { } // 可能导致死锁
// ❌ 不推荐:锁定 Type 对象
lock (typeof(MyClass)) { } // 影响范围太广
// ❌ 不推荐:锁定 this
lock (this) { } // 外部代码可能也锁定同一对象
10.2 避免常见陷阱
// ❌ 错误:锁定可变引用
object lockObj = GetLockObject();
lock (lockObj) { } // lockObj 可能被修改
// ✅ 正确:锁定不变引用
private readonly object _lock = new object();
lock (_lock) { }
// ❌ 错误:在锁内等待
lock (_lock)
{
Task.Delay(1000).Wait(); // 长时间占用锁
}
// ✅ 正确:最小化锁持有时间
var data = PrepareData();
lock (_lock)
{
UpdateSharedState(data); // 快速更新
}
10.3 死锁避免
// 使用一致的锁顺序
private static readonly object _lock1 = new object();
private static readonly object _lock2 = new object();
// ✅ 总是按相同顺序获取锁
public void Method1()
{
lock (_lock1)
{
lock (_lock2)
{
// 工作代码
}
}
}
public void Method2()
{
lock (_lock1) // 相同的顺序
{
lock (_lock2)
{
// 工作代码
}
}
}
11. 与其他同步原语的比较
11.1 vs Mutex
| 特性 | lock/Monitor | Mutex |
|---|---|---|
| 跨进程 | ❌ | ✅ |
| 性能 | 高 | 低 |
| 可重入 | ✅ | ✅ |
| 异常安全 | ✅(with using) | ✅ |
11.2 vs ReaderWriterLockSlim
| 特性 | lock/Monitor | ReaderWriterLockSlim |
|---|---|---|
| 读写分离 | ❌ | ✅ |
| 实现复杂度 | 低 | 高 |
| 读操作性能 | 中 | 高(无写入时) |
| 写操作性能 | 高 | 中 |
11.3 vs SemaphoreSlim
| 特性 | lock/Monitor | SemaphoreSlim |
|---|---|---|
| 资源计数 | 1 | N |
| 异步支持 | ❌ | ✅ |
| 性能开销 | 低 | 中 |
12. 调试和诊断
12.1 死锁检测
// 使用 ConcurrentExclusiveSchedulerPair 检测死锁
var scheduler = new ConcurrentExclusiveSchedulerPair();
// 在调试版本中添加锁超时
#if DEBUG
if (!Monitor.TryEnter(_lock, TimeSpan.FromSeconds(30)))
{
throw new TimeoutException("可能发生死锁");
}
#else
Monitor.Enter(_lock);
#endif
12.2 性能分析
// 测量锁竞争
private long _lockContentions = 0;
public void Method()
{
if (!Monitor.TryEnter(_lock, 0))
{
Interlocked.Increment(ref _lockContentions);
Monitor.Enter(_lock);
}
try
{
// 临界区代码
}
finally
{
Monitor.Exit(_lock);
}
}
13. 总结
.NET 的 lock 语句是一个精心设计的同步机制,它通过以下关键技术实现了高性能:
- 混合锁设计:从轻量级 thin lock 开始,必要时升级为重量级 fat lock
- 对象头优化:巧妙利用对象头空间存储锁信息
- 自适应策略:根据竞争情况动态调整自旋和等待策略
- 内存模型保证:提供正确的获取-释放语义
- 编译器优化:支持锁消除和锁粗化等优化技术
这些设计使得 lock 语句成为 .NET 中最常用且最可靠的同步原语,为多线程编程提供了简单而高效的解决方案。
本文来自博客园,作者:MadLongTom,转载请注明原文链接:https://www.cnblogs.com/madtom/p/19044704
浙公网安备 33010602011771号