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

news/2024/9/28 19:28:01

一:背景

1. 讲故事

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

二:Monitor.Wait 底层怎么玩的

1. 案例演示

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

internal class Program{static Person lockObject = new Person();static void Main(){Task.Run(() => { Worker1(); });Task.Run(() => { Worker2(); });Console.ReadLine();}static void Worker1(){lock (lockObject){Console.WriteLine($"{DateTime.Now} 1. 执行 worker1 的业务逻辑...");Thread.Sleep(1000);Console.WriteLine($"{DateTime.Now} 2. 等待 worker2 执行完毕...");Monitor.Wait(lockObject);Console.WriteLine($"{DateTime.Now} 4. 继续执行 worker1 的业务逻辑...");}}static void Worker2(){Thread.Sleep(10);lock (lockObject){Console.WriteLine($"{DateTime.Now} 3. worker2 的逻辑执行完毕...");Monitor.Pulse(lockObject);}}}public class Person { }

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

2. 模型架构图

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

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


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

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

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

3. 底层的源码验证

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


BOOL SyncBlock::Wait(INT32 timeOut)
{//步骤1WaitEventLink* walk = pCurThread->WaitEventLinkForSyncBlock(this);//步骤2CLREvent* hEvent = &(pCurThread->m_EventWait);waitEventLink.m_WaitSB = this;waitEventLink.m_EventWait = hEvent;waitEventLink.m_Thread = pCurThread;waitEventLink.m_Next = NULL;waitEventLink.m_LinkSB.m_pNext = NULL;waitEventLink.m_RefCount = 1;pWaitEventLink = &waitEventLink;walk->m_Next = pWaitEventLink;hEvent->Reset();//步骤3ThreadQueue::EnqueueThread(pWaitEventLink, this);isEnqueued = TRUE;PendingSync syncState(walk);OBJECTREF obj = m_Monitor.GetOwningObject();m_Monitor.IncrementTransientPrecious();//步骤4syncState.m_EnterCount = LeaveMonitorCompletely();isTimedOut = pCurThread->Block(timeOut, &syncState);return !isTimedOut;
}

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

  1. 从当前线程的 m_WaitEventLink 所指向的队列中寻找 SyncBlock 节点,如果没有就返回尾部节点。
  2. 将当前节点拼接到尾部。
  3. 新节点通过 EnqueueThread 方法送入到 m_LinkSB 所指向的队列,这里有一个小技巧,它只存放 WaitEventLink->m_LinkSB 地址,后续会通过 -0x20 来反推 WaitEventLink 结构首地址,从而来获取线程等待事件,参考代码如下:

inline PTR_WaitEventLink ThreadQueue::WaitEventLinkForLink(PTR_SLink pLink)
{LIMITED_METHOD_CONTRACT;SUPPORTS_DAC;return (PTR_WaitEventLink) (((PTR_BYTE) pLink) - offsetof(WaitEventLink, m_LinkSB));
}
  1. 使用 LeaveMonitorCompletely 方法将 AwareLock 锁给释放掉,从而让等待这个 lock 的线程进入方法,即当前的 Worker2,简化后代码如下:

LONG LeaveMonitorCompletely()
{return m_Monitor.LeaveCompletely();
}void Signal()
{m_SemEvent.SetMonitorEvent();
}void CLREventBase::SetMonitorEvent(){Set();
}

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


void SyncBlock::Pulse()
{WaitEventLink* pWaitEventLink;if ((pWaitEventLink = ThreadQueue::DequeueThread(this)) != NULL)pWaitEventLink->m_EventWait->Set();
}// Unlink the head of the Q.  We are always in the SyncBlock's critical
// section.
/* static */
inline WaitEventLink *ThreadQueue::DequeueThread(SyncBlock *psb)
{WaitEventLink* ret = NULL;SLink* pLink = psb->m_Link.m_pNext;if (pLink){psb->m_Link.m_pNext = pLink->m_pNext;ret = WaitEventLinkForLink(pLink);}return ret;
}inline PTR_WaitEventLink ThreadQueue::WaitEventLinkForLink(PTR_SLink pLink)
{return (PTR_WaitEventLink)(((PTR_BYTE)pLink) - offsetof(WaitEventLink, m_LinkSB));
}class SyncBlock
{protected:SLink m_Link;
}

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


0:017> dt coreclr!WaitEventLink+0x000 m_WaitSB         : Ptr64 SyncBlock+0x008 m_EventWait      : Ptr64 CLREvent+0x010 m_Thread         : Ptr64 Thread+0x018 m_Next           : Ptr64 WaitEventLink+0x020 m_LinkSB         : SLink+0x028 m_RefCount       : Uint4B

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


void SyncBlock::PulseAll()
{WaitEventLink* pWaitEventLink;while ((pWaitEventLink = ThreadQueue::DequeueThread(this)) != NULL)pWaitEventLink->m_EventWait->Set();
}

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

三:总结

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


图片名称

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/45650.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

Sql注入基础

1. Sql 注入基础 1.1 SQL 注入的发生1.2 如何获取数据库信息show 命令 select +函数 系统库1.3 参数会如何处理?1.4 Sql 注入的完整流程判断是否可以注入 获得数据库名 获得表名 获取列名 获得数据2. SQL 注入自动化工具 2.1 sqlmap2.2 sqlmap 参数详解 sqlmap -hh 基本用法:…

易基因:MeRIP-seq等揭示RNA m6A去甲基化酶调控植物雄性不育的分子机制 | 科研速递

大家好,这里是专注表观组学十余年,领跑多组学科研服务的易基因。 水稻是全球重要的农作物,也是单子叶植物模型。在水稻中,N6-甲基腺苷(m6A)mRNA修饰对植物的发育和胁迫响应至关重要。OsFIP37作为m6A甲基化复合体的核心组分,其缺乏会导致雄性不育,强调了m6A在雄性生育中…

硬件开发笔记(二十):AD21导入外部下载的元器件原理图库、封装库和3D模型

前言在硬件设计的过程中,会遇到一些元器件,这些元器件在本地已有的库里面没有,但是可以从外部下载或者获取到对应的。  本篇就是引入TPS54331D电源芯片作为示例,详细描述整个过程。 创建TPS54331D步骤一:下载TPS54331D模型云汉芯城  注意:无需注册登录,搜索到有,就…

2024-06-20 HarmonyOs开发初体验

2024华为开发者大会将于东莞松山湖举行,为此,特写此文。记录自己第一天入坑鸿蒙开发。鸿蒙开发简述:鸿蒙开发是指针对华为开发的一款全场景分布式操作系统的应用、服务和功能的开发工作,该操作系统名为鸿蒙,英文名为HarmonyOs。 官网地址:https://hmxt.org/ 开发工具下载…

[笔记]Splay树

前置知识:树的左旋、右旋。 Splay树是一种平衡树。能够做到每个操作均摊\(O(\log N)\)。 前言 与上文AVL树不同之处在于,AVL树在任何操作结束后,都能保证每个节点的左右子树高度相差不超过\(1\)。相应地,每个操作都是严格的\(O(\log N)\)。而Splay树并没有对“平衡”的确切…

可以免费领取tokens的大模型服务

本文更新时间:2024年6月20日 豆包大模型 "亲爱的客户,模型提供方将在5月15日至8月30日期间,为您提供一次独特的机会,即高达5亿tokens的免费权益。这是我们对您长期支持的感谢,也是对未来合作的期待。" 在8月30日之前可以领取5亿tokensDeepSeek | 深度求索 注册获…

2024欧洲杯足球分析软件推荐

前言在欧洲杯的热潮中,德国队以5比1的辉煌战绩点燃了赛事激情。对于广大足球迷和投注者来说,这不仅是一场视觉盛宴,更是一次智慧与运气的较量。在纷繁复杂的预测信息面前,你是否也曾感到迷茫?是否也曾因为媒体的喧嚣而失去了自己的判断?今天,笔者将分享一款AI智能足球分…

RPA,一个可以帮助到各行各业提效的工具

什么是RPA? RPA,即机器人流程自动化,是一种通过模拟人类在计算机上的操作,实现重复性、繁琐任务自动化的软件解决方案。RPA技术可以模拟人类用户的操作,如点击鼠标、输入数据、读取和理解屏幕信息等,以自动执行各种业务流程,它结合了API和用户界面(UI)互动,整合并执行企业…