C# 中的 `lock` 关键字本质
C# 中的 lock
关键字本质上是基于 Monitor 类实现的线程同步机制,其核心是通过 互斥锁(Mutex) 确保代码块的原子性执行。以下是其实现本质的分步解析:
1. 语法糖的转换
当使用 lock
关键字时:
lock (obj)
{// 临界区代码
}
编译器会将其转换为以下代码结构:
object __lockObj = obj;
bool __lockTaken = false;
try
{Monitor.Enter(__lockObj, ref __lockTaken);// 临界区代码
}
finally
{if (__lockTaken)Monitor.Exit(__lockObj);
}
Monitor.Enter
:尝试获取对象的锁(若锁已被占用,当前线程会阻塞)。Monitor.Exit
:释放锁,确保锁在异常情况下也能正确释放。
2. 底层同步机制
Monitor
的底层实现依赖操作系统的同步原语:
- Windows:使用 Critical Section(用户态轻量级锁,避免内核态切换的开销)。
- Linux/macOS:通过 pthread_mutex_t(POSIX 线程互斥锁)实现。
- 自旋(Spinning)优化:在进入内核态阻塞线程前,
Monitor
会短暂自旋(循环尝试获取锁),减少上下文切换的开销。
3. 锁对象的要求
- 必须是引用类型:值类型会被装箱,导致每次锁定的对象不同,失去同步作用。
- 建议使用私有对象:避免外部代码意外锁定同一对象,引发死锁。
private readonly object _syncLock = new object(); // 正确做法
4. 线程状态管理
- 锁竞争:当线程调用
Monitor.Enter
时:- 如果锁空闲,线程获取锁并继续执行。
- 如果锁被占用,线程进入 等待队列(Wait Queue),状态变为
Blocked
。
- 锁释放:调用
Monitor.Exit
时:- 唤醒等待队列中的下一个线程(若有)。
- 若使用
Monitor.Pulse
/PulseAll
,可显式控制线程唤醒(常用于生产者-消费者模式)。
5. 可重入性(Reentrancy)
- 同一线程可重复获取锁:C# 的
Monitor
允许线程多次进入同一个锁(重入),计数器记录锁定次数,需对应次数的Exit
调用才能完全释放。lock (obj) {// 可再次锁定同一对象lock (obj){// 代码...} }
6. 性能与注意事项
- 避免锁粒度过大:锁定的代码块应尽量小,减少线程阻塞时间。
- 避免嵌套锁:可能引发死锁(需确保锁的获取顺序一致)。
- 替代方案:在特定场景下,可使用
Semaphore
、Mutex
或ReaderWriterLockSlim
等更灵活的同步机制。
总结
C# 的 lock
本质是通过 Monitor
类实现的互斥锁,结合用户态和内核态的同步机制,确保多线程环境下的代码原子性执行。其设计在易用性和性能之间取得平衡,但需开发者合理使用以避免死锁和性能问题。