聊一聊 Monitor.Wait 和 Pulse 的底层玩法 - 一线码农

聊一聊 Monitor.Wait 和 Pulse 的底层玩法 - 一线码农

💡 原文中文,约4700字,阅读约需12分钟。
📝

内容提要

本文探讨了C#中Monitor.Wait和Monitor.Pulse的实现,分析了线程如何在Monitor.Wait上等待以及如何通过Monitor.Pulse唤醒等待的线程。重点介绍了WaitEventLink数据结构及其在同步块中的作用,揭示了线程等待和唤醒的内部逻辑,以帮助更好地理解多线程编程。

🔎

延伸解读

WaitEventLink 与双队列设计

文章指出 Monitor.Wait 的核心是 WaitEventLink 结构,它同时挂在两个队列上:通过 m_Next 链接同一线程等待的所有 SyncBlock,通过 m_LinkSB 链接同一 SyncBlock 上等待的所有线程。这种双队列设计使得线程可以等待多个对象,而对象也能管理多个等待线程。理解这一点,就能明白为什么 !syncblk 看不到等待锁信息——等待线程已从 Monitor 的拥有者队列移出,转入 SyncBlock 的等待队列。

Pulse 唤醒的精确性

Monitor.Pulse 只从 SyncBlock.m_Link 队列头部取出一个 WaitEventLink,并调用其 m_EventWait->Set() 唤醒对应线程。这意味着 Pulse 的唤醒是精确且有序的(FIFO),不会唤醒所有等待者。而 PulseAll 则循环取出所有节点并逐一唤醒。这种差异直接影响线程调度顺序,在需要避免惊群效应时,Pulse 是更可控的选择。

等待队列的清理时机

文章提到,被 Pulse 唤醒的线程并不会立即从另一个队列(线程自身的等待链)中移除,而是在 Wait 方法的 PendingSync 析构函数中通过 Restore 方法完成清理。这解释了为什么在 dump 中可能看到已唤醒线程仍短暂保留在等待链上。理解这一延迟清理机制,有助于更准确地分析线程状态和锁竞争问题。

❓

Q&A

Monitor.Wait方法的作用是什么?

Monitor.Wait方法用于让线程在特定对象上等待,直到被唤醒。

WaitEventLink数据结构在Monitor.Wait中有什么作用?

WaitEventLink数据结构用于管理线程等待的事件,支持线程的等待和唤醒逻辑。

Monitor.Pulse和Monitor.PulseAll有什么区别?

Monitor.Pulse唤醒等待队列中的一个线程,而Monitor.PulseAll会唤醒所有等待的线程。

Monitor.Wait的内部实现是怎样的?

Monitor.Wait的内部实现涉及将Node追加到两个队列,并通过WaitEventLink管理线程的等待状态。

如何使用Monitor.Wait和Monitor.Pulse进行多线程同步?

使用Monitor.Wait可以让线程等待特定条件,而Monitor.Pulse则用于通知等待的线程继续执行,从而实现多线程同步。

Monitor.Wait和Monitor.Pulse的理解对多线程编程有什么帮助?

理解Monitor.Wait和Monitor.Pulse的内部实现有助于掌握多线程编程中的同步机制,提高程序的效率和稳定性。

🏷️

标签

➡️

继续阅读