聊一聊 Monitor.Wait 和 Pluse 的底层玩法
内容提要
本文主要讲述了在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,并设置其对应的事件,从而唤醒等待的线程。