经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C++ » 查看文章
聊一聊 Monitor.Wait 和 Pulse 的底层玩法
来源:cnblogs  作者:一线码农  时间:2024/6/25 8:55:50  对本文有异议

一:背景

1. 讲故事

在dump分析的过程中经常会看到很多线程卡在Monitor.Wait方法上,曾经也有不少人问我为什么用 !syncblk 看不到 Monitor.Wait 上的锁信息,刚好昨天有时间我就来研究一下。

二:Monitor.Wait 底层怎么玩的

1. 案例演示

为了方便讲述,先上一段演示代码,Worker1 在执行的过程中需要唤醒 Worker2 执行,当 Worker2 执行完毕之后自己再继续执行,参考代码如下:

  1. internal class Program
  2. {
  3. static Person lockObject = new Person();
  4. static void Main()
  5. {
  6. Task.Run(() => { Worker1(); });
  7. Task.Run(() => { Worker2(); });
  8. Console.ReadLine();
  9. }
  10. static void Worker1()
  11. {
  12. lock (lockObject)
  13. {
  14. Console.WriteLine($"{DateTime.Now} 1. 执行 worker1 的业务逻辑...");
  15. Thread.Sleep(1000);
  16. Console.WriteLine($"{DateTime.Now} 2. 等待 worker2 执行完毕...");
  17. Monitor.Wait(lockObject);
  18. Console.WriteLine($"{DateTime.Now} 4. 继续执行 worker1 的业务逻辑...");
  19. }
  20. }
  21. static void Worker2()
  22. {
  23. Thread.Sleep(10);
  24. lock (lockObject)
  25. {
  26. Console.WriteLine($"{DateTime.Now} 3. worker2 的逻辑执行完毕...");
  27. Monitor.Pulse(lockObject);
  28. }
  29. }
  30. }
  31. public class Person { }

有了代码和输出之后,接下来就是分析底层玩法了。

2. 模型架构图

研究来研究去总得有个结果,千言万语绘成一张图,截图如下:

从图中可以看到这地方会涉及到一个核心的数据结构 WaitEventLink,参考如下:

  1. // Used inside Thread class to chain all events that a thread is waiting for by Object::Wait
  2. struct WaitEventLink {
  3. SyncBlock *m_WaitSB; // 当前对象的 syncblock
  4. CLREvent *m_EventWait; // 当前线程的 m_EventWait
  5. PTR_Thread m_Thread; // Owner of this WaitEventLink.
  6. PTR_WaitEventLink m_Next; // Chain to the next waited SyncBlock.
  7. SLink m_LinkSB; // Chain to the next thread waiting on the same SyncBlock.
  8. DWORD m_RefCount; // How many times Object::Wait is called on the same SyncBlock.
  9. };

代码里对每一个字段都做了表述,还是非常清楚的,也看到了这里存在两个队列。

  1. m_Next: 当前线程要串联的 SyncBlock 队列,Node 是 WaitEventLink 结构。
  2. m_LinkSB:当前同步块串联的 Thread 队列,Node 是 m_LinkSB 地址。

3. 底层的源码验证

首先我们看下C#的 Monitor.Wait(lockObject) 底层是如何实现的,它对应着 coreclr 的 ObjectNative::WaitTimeout 方法,核心实现如下:

  1. BOOL SyncBlock::Wait(INT32 timeOut)
  2. {
  3. //步骤1
  4. WaitEventLink* walk = pCurThread->WaitEventLinkForSyncBlock(this);
  5. //步骤2
  6. CLREvent* hEvent = &(pCurThread->m_EventWait);
  7. waitEventLink.m_WaitSB = this;
  8. waitEventLink.m_EventWait = hEvent;
  9. waitEventLink.m_Thread = pCurThread;
  10. waitEventLink.m_Next = NULL;
  11. waitEventLink.m_LinkSB.m_pNext = NULL;
  12. waitEventLink.m_RefCount = 1;
  13. pWaitEventLink = &waitEventLink;
  14. walk->m_Next = pWaitEventLink;
  15. hEvent->Reset();
  16. //步骤3
  17. ThreadQueue::EnqueueThread(pWaitEventLink, this);
  18. isEnqueued = TRUE;
  19. PendingSync syncState(walk);
  20. OBJECTREF obj = m_Monitor.GetOwningObject();
  21. m_Monitor.IncrementTransientPrecious();
  22. //步骤4
  23. syncState.m_EnterCount = LeaveMonitorCompletely();
  24. isTimedOut = pCurThread->Block(timeOut, &syncState);
  25. return !isTimedOut;
  26. }

代码逻辑非常简单,大概步骤如下:

  1. 从当前线程的 m_WaitEventLink 所指向的队列中寻找 SyncBlock 节点,如果没有就返回尾部节点。
  2. 将当前节点拼接到尾部。
  3. 新节点通过 EnqueueThread 方法送入到 m_LinkSB 所指向的队列,这里有一个小技巧,它只存放 WaitEventLink->m_LinkSB 地址,后续会通过 -0x20 来反推 WaitEventLink 结构首地址,从而来获取线程等待事件,参考代码如下:
  1. inline PTR_WaitEventLink ThreadQueue::WaitEventLinkForLink(PTR_SLink pLink)
  2. {
  3. LIMITED_METHOD_CONTRACT;
  4. SUPPORTS_DAC;
  5. return (PTR_WaitEventLink) (((PTR_BYTE) pLink) - offsetof(WaitEventLink, m_LinkSB));
  6. }
  1. 使用 LeaveMonitorCompletely 方法将 AwareLock 锁给释放掉,从而让等待这个 lock 的线程进入方法,即当前的 Worker2,简化后代码如下:
  1. LONG LeaveMonitorCompletely()
  2. {
  3. return m_Monitor.LeaveCompletely();
  4. }
  5. void Signal()
  6. {
  7. m_SemEvent.SetMonitorEvent();
  8. }
  9. void CLREventBase::SetMonitorEvent(){
  10. Set();
  11. }

总而言之,Monitor.Wait 主要还是用来将Node追加到两大队列,接下来研究下 Monitor.Pulse 的内部实现,这个就比较简单了,无非就是在 m_LinkSB 指向的队列中提取一个Node而已,核心代码如下:

  1. void SyncBlock::Pulse()
  2. {
  3. WaitEventLink* pWaitEventLink;
  4. if ((pWaitEventLink = ThreadQueue::DequeueThread(this)) != NULL)
  5. pWaitEventLink->m_EventWait->Set();
  6. }
  7. // Unlink the head of the Q. We are always in the SyncBlock's critical
  8. // section.
  9. /* static */
  10. inline WaitEventLink *ThreadQueue::DequeueThread(SyncBlock *psb)
  11. {
  12. WaitEventLink* ret = NULL;
  13. SLink* pLink = psb->m_Link.m_pNext;
  14. if (pLink)
  15. {
  16. psb->m_Link.m_pNext = pLink->m_pNext;
  17. ret = WaitEventLinkForLink(pLink);
  18. }
  19. return ret;
  20. }
  21. inline PTR_WaitEventLink ThreadQueue::WaitEventLinkForLink(PTR_SLink pLink)
  22. {
  23. return (PTR_WaitEventLink)(((PTR_BYTE)pLink) - offsetof(WaitEventLink, m_LinkSB));
  24. }
  25. class SyncBlock
  26. {
  27. protected:
  28. SLink m_Link;
  29. }

上面的代码逻辑还是非常清楚的,从 SyncBlock.m_Link 所串联的 WaitEventLink 队列中提取第一个节点,但这个节点保存的是 WaitEventLink.m_LinkSB 地址,所以需要反向 -0x20 取到 WaitEventLink 首地址,可以用 windbg 来验证一下。

  1. 0:017> dt coreclr!WaitEventLink
  2. +0x000 m_WaitSB : Ptr64 SyncBlock
  3. +0x008 m_EventWait : Ptr64 CLREvent
  4. +0x010 m_Thread : Ptr64 Thread
  5. +0x018 m_Next : Ptr64 WaitEventLink
  6. +0x020 m_LinkSB : SLink
  7. +0x028 m_RefCount : Uint4B

取到首地址之后就就可以将当前线程的 m_EventWait 唤醒,这就是为什么调用 Monitor.Pulse(lockObject); 之后另一个线程唤醒的内部逻辑,有些朋友好奇那 Monitor.PulseAll 是不是会把这个队列中的所有 Node 上的 m_EventWait 都唤醒呢?哈哈,真聪明,源码如下:

  1. void SyncBlock::PulseAll()
  2. {
  3. WaitEventLink* pWaitEventLink;
  4. while ((pWaitEventLink = ThreadQueue::DequeueThread(this)) != NULL)
  5. pWaitEventLink->m_EventWait->Set();
  6. }

眼尖的朋友会有一个疑问,这个队列数据提取了,那另一个队列的数据是不是也要相应的改动,这个确实,它的逻辑是在Wait方法的 PendingSync syncState(walk); 析构函数里,感兴趣的朋友可以看一下内部的void Restore(BOOL bRemoveFromSB) 方法即可。

三:总结

花了半天研究这东西还是挺有意思的,重点还是要理解下那张图,理解了之后我相信你对 Monitor.Pluse 方法注释中所指的 waiting queue 会有一个新的体会。


图片名称

原文链接:https://www.cnblogs.com/huangxincheng/p/18258390

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号