聊一聊 Monitor.Wait 和 Pluse 的底层玩法

💡 原文中文,约5800字,阅读约需14分钟。
📝

内容提要

本文主要讲述了在dump分析过程中经常会看到线程卡在Monitor.Wait方法上的情况,以及为什么用!syncblk看不到Monitor.Wait上的锁信息。通过分析底层源码,发现Monitor.Wait主要是将Node追加到两个队列中,而Monitor.Pulse则是从队列中提取一个Node进行唤醒。同时,还介绍了Monitor.PulseAll方法的实现。

🔎

延伸解读

为什么 !syncblk 看不到 Monitor.Wait 的锁信息

在 dump 分析中,线程卡在 Monitor.Wait 时,用 !syncblk 看不到锁信息,这是因为 Monitor.Wait 会将线程的等待节点从 SyncBlock 的锁队列中移出,并挂到另外两个队列上。具体来说,WaitEventLink 结构通过 m_Next 和 m_LinkSB 分别串联线程等待的 SyncBlock 和同步块上等待的线程。因此,!syncblk 只显示当前持有锁的线程,而等待中的线程信息需要查看这些队列。

WaitEventLink 的双队列设计

WaitEventLink 是 Monitor.Wait 底层实现的核心数据结构,它包含两个队列链接:m_Next 用于将当前线程等待的所有 SyncBlock 串联成队列,m_LinkSB 用于将等待同一个 SyncBlock 的所有线程串联成队列。这种双队列设计使得 Monitor.Wait 和 Monitor.Pulse 能够高效地管理线程等待和唤醒。理解这两个队列的指向关系,是分析 dump 中线程状态的关键。

Monitor.Pulse 与 PulseAll 的唤醒机制

Monitor.Pulse 从 SyncBlock 的 m_Link 队列中取出第一个 WaitEventLink 节点,并通过其 m_EventWait 唤醒对应线程。由于队列中存储的是 m_LinkSB 地址,需要通过偏移 -0x20 反推 WaitEventLink 首地址。Monitor.PulseAll 则循环取出所有节点并逐一唤醒。这种机制解释了为什么 Pulse 只唤醒一个等待线程,而 PulseAll 会唤醒全部。

队列清理与 PendingSync 的作用

当线程被唤醒后,需要从另一个队列中移除对应的 WaitEventLink 节点,这一操作在 Wait 方法的 PendingSync 析构函数中完成,具体由 Restore 方法实现。如果忽略这一步,会导致队列中残留无效节点,影响后续的等待和唤醒逻辑。因此,理解 PendingSync 的清理机制对于完整掌握 Monitor.Wait 和 Pulse 的底层行为至关重要。

❓

Q&A

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

Monitor.Wait方法主要用于将当前线程的Node追加到等待队列中,导致线程进入等待状态。

为什么使用!syncblk无法看到Monitor.Wait上的锁信息?

!syncblk无法显示Monitor.Wait上的锁信息是因为Monitor.Wait将线程的状态转移到等待队列中,导致锁信息不可见。

Monitor.Pulse和Monitor.PulseAll的区别是什么?

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

Monitor.Wait的底层实现涉及哪些步骤?

Monitor.Wait的底层实现包括查找SyncBlock节点、拼接当前节点、将节点送入等待队列等步骤。

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

WaitEventLink结构用于链式存储线程等待的事件,帮助管理线程的等待状态。

如何通过Monitor.Pulse唤醒等待的线程?

通过Monitor.Pulse,系统从等待队列中提取一个Node,并设置其对应的事件,从而唤醒等待的线程。

➡️

继续阅读