一:背景

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; // 当前对象的 syncblock
CLREvent *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)
{
//步骤1
WaitEventLink* walk = pCurThread->WaitEventLinkForSyncBlock(this); //步骤2
CLREvent* 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(); //步骤3
ThreadQueue::EnqueueThread(pWaitEventLink, this); isEnqueued = TRUE;
PendingSync syncState(walk); OBJECTREF obj = m_Monitor.GetOwningObject(); m_Monitor.IncrementTransientPrecious(); //步骤4
syncState.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 会有一个新的体会。



聊一聊 Monitor.Wait 和 Pluse 的底层玩法的更多相关文章

  1. C#语法糖系列 —— 第一篇:聊聊 params 参数底层玩法

    首先说说为什么要写这个系列,大概有两点原因. 这种文章阅读量确实高... 对 IL 和 汇编代码 的学习巩固 所以就决定写一下这个系列,如果大家能从中有所收获,那就更好啦! 一:params 应用层玩 ...

  2. C#语法糖系列 —— 第三篇:聊聊闭包的底层玩法

    有朋友好奇为什么将 闭包 归于语法糖,这里简单声明下,C# 中的所有闭包最终都会归结于 类 和 方法,为什么这么说,因为 C# 的基因就已经决定了,如果大家了解 CLR 的话应该知道, C#中的类最终 ...

  3. 聊聊 C# 方法重载的底层玩法

    最近在看 C++ 的方法重载,我就在想 C# 中的重载底层是怎么玩的,很多朋友应该知道 C 是不支持重载的,比如下面的代码就会报错. #include <stdio.h> int say( ...

  4. 聊聊 C# 和 C++ 中的 泛型模板 底层玩法

    最近在看 C++ 的方法和类模板,我就在想 C# 中也是有这个概念的,不过叫法不一样,人家叫模板,我们叫泛型,哈哈,有点意思,这一篇我们来聊聊它们底层是怎么玩的? 一:C++ 中的模板玩法 毕竟 C+ ...

  5. C#语法糖系列 —— 第二篇:聊聊 ref,in 修饰符底层玩法

    自从 C# 7.3 放开 ref 之后,这玩法就太花哨了,也让 C# 这门语言变得越来越多范式,越来越重,这篇我们就来聊聊 ref,本质上来说 ref 的放开就是把 C/C++ 指针的那一套又拿回来了 ...

  6. 全面解密QQ红包技术方案:架构、技术实现、移动端优化、创新玩法等

    本文来自腾讯QQ技术团队工程师许灵锋.周海发的技术分享. 一.引言 自 2015 年春节以来,QQ 春节红包经历了企业红包(2015 年).刷一刷红包(2016 年)和 AR 红包(2017 年)几个 ...

  7. 朱晔的互联网架构实践心得S2E4:小议微服务的各种玩法(古典、SOA、传统、K8S、ServiceMesh)

    十几年前就有一些公司开始践行服务拆分以及SOA,六年前有了微服务的概念,于是大家开始思考SOA和微服务的关系和区别.最近三年Spring Cloud的大火把微服务的实践推到了高潮,而近两年K8S在容器 ...

  8. C#9.0 终于来了,带你一起解读Pattern matching 和 nint 两大新特性玩法

    一:背景 1. 讲故事 上一篇跟大家聊到了Target-typed new 和 Lambda discard parameters,看博客园和公号里的阅读量都达到了新高,甚是欣慰,不管大家对新特性是多 ...

  9. [C#] 软硬结合第二篇——酷我音乐盒的逆天玩法

    1.灵感来源: LZ是纯宅男,一天从早上8:00起一直要呆在电脑旁到晚上12:00左右吧~平时也没人来闲聊几句,刷空间暑假也没啥动态,听音乐吧...~有些确实不好听,于是就不得不打断手头的工作去点击下 ...

  10. git分布式版本控制玩法

    git分布式版本控制玩法 Git distributed version control play github的配置安装步骤:1.下载git bash(从http://www.git-scm.com ...

随机推荐

  1. 尝试 IIncrementalGenerator 进行增量 Source Generator 生成代码

    在加上热重载时,源代码生成 Source Generator 的默认行为会让 Visual Studio 有些为难,其原因是热重载会变更代码,变更代码触发代码生成器更新代码,代码生成器更新的代码说不定 ...

  2. Go类型断言demo

    Go类型断言demo package main import ( "bytes" "encoding/json" "fmt" "i ...

  3. 机器学习策略:详解什么时候该改变开发/测试集和指标?(When to change dev/test sets and metrics)

    什么时候该改变开发/测试集和指标? 有时候在项目进行途中,可能意识到,目标的位置放错了.这种情况下,应该移动的目标. 来看一个例子,假设在构建一个猫分类器,试图找到很多猫的照片,向的爱猫人士用户展示, ...

  4. SQL Server 数据库分析操作程序时SQL语句等待或执行时间较长(死锁)

    大型程序,总会遇到程序操作端卡顿或者直接遇到死锁,死锁是数据库设计的缺陷,虽不能完全避免死锁的出现,但一旦出现死锁我们要进行及时分析死锁语句,找出原因,进行SQL语句或程序的优化,避免再次出现同样的问 ...

  5. Calcite sql2rel 过程

    sql2rel的过程是将SqlNode 转化成RelNode的过程 在 SqlToRelConverterTest中添加样例测试 @Test void testScan() { String sql ...

  6. apisix~lua插件开发与插件注册

    开发插件的步骤 在APISIX中,要自定义插件,一般需要按照以下步骤进行操作: 编写Lua脚本:首先,你需要编写Lua脚本来实现你想要的功能.可以根据APISIX提供的插件开发文档和示例进行编写. 将 ...

  7. PAT 练习2-3 输出倒三角图案

    结果: 本题要求编写程序,输出指定的由"*"组成的倒三角图案. 输入格式: 本题目没有输入. 输出格式: 按照下列格式输出由"*"组成的倒三角图案. 一般都用的 ...

  8. JDK源码阅读-------自学笔记(二十一)(java.util.ArrayList详细版集合类)

    一.前景提要 本人经历了IT行业的多种类型企业,外包/创业小公司/中型公司,一步步成长起来,现在可以给大家透露下为什么进大企业在IT行业是重要的: 在外包公司,你要做的就是对接别人写好的接口,然后按照 ...

  9. centos7系统的七个运行级别和设置默认运行级别

    一.系统七个运行级别概述 0 系统停机模式,系统默认运行级别不能设置为0,否则不能正常启动,机器关的 1 单用户模式,root权限,用于系统维护,禁止远程登陆,就像Windows下的安全模式登录 2 ...

  10. Java中枚举类的深入理解

    概述 在此之前由于个人在学习和开发中对枚举类的应用较少,所以对枚举类的知识点并没进行深入的了解,但最近写代码中突然想到了枚举类,抱着完善自身的知识的目的,就较为深入的了解了Java中的枚举类. 我在学 ...