多线程六脉神剑第二剑:监视器锁 (Monitor)
文章目录
- 1、举栗子🌰
- 2、Monitor 的本质
- 2.1 对象头中的秘密
- 2.2 对象与锁的关联
- 3、核心工作原理
- 3.1 锁的获取流程
- 3.2 锁的释放流程
- 4、Monitor 的完整使用
- 4.1 基础用法 - lock 语法糖
- 4.2 高级功能 - Wait/Pulse 机制
- 4.3 超时控制 - TryEnter
- 5、Wait/Pulse 的详细机制
- 5.1 等待队列的工作原理
- 5.2 复杂的协调示例
- 6、Monitor 的底层优化
- 6.1 瘦锁(Thin Lock)
- 6.2 自旋等待
- 7、最佳实践和陷阱
- 7.1 正确做法
- 7.2 常见陷阱
- 8、总结
1、举栗子🌰
场景:公司的唯一打印机
打印机是共享资源
-
Monitor.Enter() 相当于 拿到打印权限并关门
-
Monitor.Exit() 相当于 打印完成并开门
-
Monitor.Wait() 相当于 暂时离开但保留排队位置
-
Monitor.Pulse() 相当于 通知下一个人可以准备了
2、Monitor 的本质
2.1 对象头中的秘密
每个 .NET 对象内部都有一个对象头,其中包含同步块索引:
// 概念性结构 - 实际在 CLR 中实现
class ObjectHeader
{SyncBlockIndex syncBlock; // 同步块索引TypeHandle typeHandle; // 类型信息// ... 其他字段
}
同步块(SyncBlock) 才是真正的锁信息容器:
class SyncBlock
{Thread ownerThread; // 当前拥有锁的线程int recursionCount; // 重入计数WaitQueue waitQueue; // 等待队列// ... 其他同步信息
}
2.2 对象与锁的关联
object lockObj = new object(); // 任何引用类型对象都可以作为锁// 当你执行 lock(lockObj) 时:
// 1. 检查 lockObj 的对象头中的同步块索引
// 2. 如果为0,创建新的同步块并关联
// 3. 如果已有同步块,检查锁状态
// 4. 根据情况获取锁或进入等待
3、核心工作原理
3.1 锁的获取流程
// 伪代码展示 Monitor.Enter 的内部逻辑
bool MonitorEnter(object obj, ref bool lockTaken)
{// 第一步:快速路径 - 无竞争情况if (obj.SyncBlockIndex == 0) {// 使用原子操作设置同步块if (Interlocked.CompareExchange(ref obj.SyncBlockIndex, CreateSyncBlock(), 0) == 0){obj.SyncBlock.OwnerThread = CurrentThread;obj.SyncBlock.RecursionCount = 1;lockTaken = true;return true;}}// 第二步:检查是否已由当前线程持有(递归锁)if (obj.SyncBlock.OwnerThread == CurrentThread){obj.SyncBlock.RecursionCount++;lockTaken = true;return true;}// 第三步:慢速路径 - 需要等待return SlowPathMonitorEnter(obj, ref lockTaken);
}
3.2 锁的释放流程
void MonitorExit(object obj)
{// 验证当前线程确实持有锁if (obj.SyncBlock.OwnerThread != CurrentThread)throw new SynchronizationLockException();// 减少递归计数obj.SyncBlock.RecursionCount--;if (obj.SyncBlock.RecursionCount == 0){// 真正释放锁obj.SyncBlock.OwnerThread = null;// 如果有等待线程,唤醒一个if (obj.SyncBlock.WaitQueue.Count > 0){WakeOneWaitingThread(obj);}}
}
4、Monitor 的完整使用
4.1 基础用法 - lock 语法糖
class BasicUsage
{private object _lockObject = new object();private int _sharedCounter = 0;// 方式1:使用 lock 关键字(推荐)void IncrementWithLock(){lock (_lockObject) // 编译器自动生成 Monitor.Enter/Exit{_sharedCounter++;// 其他临界区代码}}// 方式2:显式使用 Monitor(等价于上面的 lock)void IncrementWithMonitor(){bool lockTaken = false;try{Monitor.Enter(_lockObject, ref lockTaken);_sharedCounter++;}finally{if (lockTaken){Monitor.Exit(_lockObject);}}}
}
为什么使用 ref bool lockTaken?
// 防止异步异常导致锁无法释放
void DangerousMethod()
{Monitor.Enter(_lockObject); // 不推荐!如果这里发生异常,锁不会被释放try{// 临界区}finally{Monitor.Exit(_lockObject);}
}void SafeMethod()
{bool lockTaken = false;try{Monitor.Enter(_lockObject, ref lockTaken); // 原子操作,要么完全成功要么完全失败// 临界区}finally{if (lockTaken) // 只有成功获取锁才释放{Monitor.Exit(_lockObject);}}
}
4.2 高级功能 - Wait/Pulse 机制
这是 Monitor 最强大的特性,用于复杂的线程协调:
class ProducerConsumerWithMonitor
{private object _lockObject = new object();private Queue<string> _queue = new Queue<string>();private bool _isStopped = false;// 生产者public void Produce(string item){lock (_lockObject){// 如果队列太大,等待消费者处理while (_queue.Count >= 5){Console.WriteLine("生产者: 队列已满,等待...");Monitor.Wait(_lockObject); // 释放锁并等待}_queue.Enqueue(item);Console.WriteLine($"生产者: 添加 {item},队列大小: {_queue.Count}");// 通知等待的消费者Monitor.Pulse(_lockObject);}}// 消费者public string Consume(){lock (_lockObject){// 如果队列为空,等待生产者while (_queue.Count == 0 && !_isStopped){Console.WriteLine("消费者: 队列为空,等待...");Monitor.Wait(_lockObject);}if (_queue.Count == 0) return null; // 已停止且队列为空string item = _queue.Dequeue();Console.WriteLine($"消费者: 取出 {item},队列大小: {_queue.Count}");// 通知等待的生产者Monitor.Pulse(_lockObject);return item;}}public void Stop(){lock (_lockObject){_isStopped = true;Monitor.PulseAll(_lockObject); // 唤醒所有等待线程}}
}// 使用示例
class Program
{static void Main(){var pc = new ProducerConsumerWithMonitor();// 启动生产者线程Thread producer = new Thread(() => {for (int i = 0; i < 10; i++){pc.Produce($"产品-{i}");Thread.Sleep(100);}});// 启动消费者线程Thread consumer = new Thread(() => {for (int i = 0; i < 10; i++){pc.Consume();Thread.Sleep(150);}});producer.Start();consumer.Start();producer.Join();consumer.Join();}
}
4.3 超时控制 - TryEnter
class TimeoutExample
{private object _lockObject = new object();public bool TryDoWork(int timeoutMs){bool lockTaken = false;try{// 尝试在指定时间内获取锁lockTaken = Monitor.TryEnter(_lockObject, timeoutMs);if (lockTaken){// 成功获取锁,执行工作DoCriticalWork();return true;}else{// 超时,执行备用方案Console.WriteLine($"获取锁超时 ({timeoutMs}ms),执行备用操作");DoFallbackWork();return false;}}finally{if (lockTaken){Monitor.Exit(_lockObject);}}}private void DoCriticalWork(){Thread.Sleep(2000); // 模拟耗时工作Console.WriteLine("关键工作完成");}private void DoFallbackWork(){Console.WriteLine("执行备用工作");}
}
5、Wait/Pulse 的详细机制
5.1 等待队列的工作原理
class WaitPulseMechanism
{private object _lockObject = new object();void DemonstrateWaitPulse(){lock (_lockObject){// 当调用 Monitor.Wait(_lockObject) 时:// 1. 当前线程释放 _lockObject 的锁// 2. 线程进入 _lockObject 的等待队列// 3. 线程状态改为 WaitSleepJoin// 当其他线程调用 Monitor.Pulse(_lockObject) 时:// 1. 从等待队列中移出一个线程// 2. 该线程进入就绪队列,准备重新获取锁// 当其他线程调用 Monitor.PulseAll(_lockObject) 时:// 1. 所有等待线程都移到就绪队列}}
}
5.2 复杂的协调示例
class ComplexCoordination
{private object _lockObject = new object();private int _currentPhase = 0;private int _threadsReady = 0;private const int TOTAL_THREADS = 3;public void WorkerThread(int threadId){for (int phase = 0; phase < 3; phase++){lock (_lockObject){// 等待进入下一阶段while (_currentPhase != phase){Monitor.Wait(_lockObject);}_threadsReady++;Console.WriteLine($"线程 {threadId} 准备就绪阶段 {phase}");// 如果所有线程都就绪,推进到下一阶段if (_threadsReady == TOTAL_THREADS){_currentPhase++;_threadsReady = 0;Console.WriteLine($"=== 所有线程完成阶段 {phase},进入阶段 {_currentPhase} ===");Monitor.PulseAll(_lockObject); // 唤醒所有线程}else{// 等待其他线程while (_currentPhase == phase){Monitor.Wait(_lockObject);}}}// 执行阶段工作Console.WriteLine($"线程 {threadId} 执行阶段 {phase} 的工作");Thread.Sleep(500);}}
}
6、Monitor 的底层优化
6.1 瘦锁(Thin Lock)
对于轻度竞争的情况,CLR 使用优化:
// 概念性优化
class ThinLock
{// 对于轻度使用,直接在对象头中存储锁信息,避免创建完整的SyncBlock// 使用位域存储:线程ID + 递归计数// 当竞争激烈时,才升级为完整的SyncBlock
}
6.2 自旋等待
在进入内核等待前,会先自旋一段时间:
bool TryEnterWithSpin(object obj, int timeoutMs)
{int spinCount = CalculateOptimalSpinCount();for (int i = 0; i < spinCount; i++){if (TryEnterFastPath(obj)) return true;Thread.SpinWait(100); // 用户态自旋,避免内核切换}return EnterSlowPath(obj, timeoutMs);
}
7、最佳实践和陷阱
7.1 正确做法
class BestPractices
{private readonly object _lockObject = new object(); // readonly 防止意外修改public void GoodPractice1(){lock (_lockObject){// 临界区代码尽量简短DoQuickOperation();}// 长时间操作放在锁外DoLongRunningOperation();}public void GoodPractice2(){// 使用明确的超时控制if (Monitor.TryEnter(_lockObject, 1000)){try{// 工作}finally{Monitor.Exit(_lockObject);}}}
}
7.2 常见陷阱
class CommonMistakes
{private object _lockObject = new object();// 错误1:锁住错误的对象public void Mistake1(){lock (this) // 不好!外部代码可能也锁住这个实例{// ...}}// 错误2:在锁内调用未知方法public void Mistake2(){lock (_lockObject){SomeExternalMethod(); // 危险!可能造成死锁}}// 错误3:忘记释放锁public void Mistake3(){Monitor.Enter(_lockObject); // 如果没有对应的Exit,会导致死锁// 应该使用 try-finally 或 lock 关键字}// 错误4:错误的 Wait/Pulse 使用public void Mistake4(){// 必须在锁内调用 Wait/PulseMonitor.Wait(_lockObject); // 错误!不在锁内}
}
8、总结
Monitor 的本质:
-
是基于对象头同步块的软件锁
-
提供递归获取能力(同一线程可多次获取)
-
内置等待/通知机制(Wait/Pulse)
-
在用户态和内核态间智能切换
核心优势:
-
与对象绑定:任何对象都可作为锁
-
递归安全:同一线程不会死锁自己
-
条件变量:内置复杂的线程协调能力
-
性能优化:瘦锁、自旋等待等优化
使用要点:
优先使用 lock 关键字
-
复杂协调使用 Wait/Pulse
-
总是用 try-finally 确保锁释放
-
避免在锁内执行耗时操作
