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

一:背景

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.mzph.cn/pingmian/30704.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Chromium 开发指南2024 Mac篇-Chromium项目编译小技巧(六)

1. 前言 在编译大型项目如 Chromium 时,优化编译速度是非常重要的。本文将介绍一些编译优化的小技巧,尤其是如何使用 ccache 来加速 C/C 代码的重新编译。ccache 是一个编译器缓存,通过缓存之前的编译并检测何时再次进行相同的编译&#xff…

“display interface“的43条信息,这条绝对被你忽略了

号主:老杨丨11年资深网络工程师,更多网工提升干货,请关注公众号:网络工程师俱乐部 大家好,我是张总。 上周和老杨唠嗑,他说我每次都是直播,或者视频号上给大家聊聊技术,都没发过技术…

U盘删除的文件怎么找回?数据恢复,5个方法

“我的u盘里有部分文件不小心被删除了,尝试了很多的方法都无法找回它们。U盘删除的文件怎么找回呢?希望大家给我出出主意!” 保存了很多重要的文件在u盘中,查看u盘内存时却发现很多文件被删除了?别慌!即使u…

B树(数据结构篇)

数据结构之B树 B-树(B-tree) 概念: B-树是一个非二叉树的多路平衡查找树(数据有序),是一颗所有数据都存储在树叶节点上的树,不一定存储具体的数据,也可以是指向包含数据的记录的指针或地址 对于**阶为M(子节点数量在2和M之间)*…

Properties与xml知识点总结

文章目录 一、Properties1.1 构造方法1.2 从Properties文件中获取1.3 向Properties文件中存储 二、xml2.1 XML2.2 特点2.3 规则2.3 抬头声明2.4 特殊字符2.5 CDATA区段2.4 作用和应用场景 三、区别 一、Properties 定义:properties是一个双列集合集合,拥…

android 在线程中更新界面

在Android中,你不能直接从子线程中更新UI,因为这会导致应用崩溃。你需要使用Handler或runOnUiThread()来更新UI。 使用Handler 以下是如何使用Handler在子线程中更新UI的示例: 1. 创建Handler实例: import android.os.Bundle;…

产品经理方法论

1、用户体验 5 要素 1,表现层是你拿到一个产品以后,视觉表现,配色,布局,排版等等 2,框架层,是交互层面的东西,比如,操作情况,刷新,页面跳转&…

ChatmoneyAI如狂风般席卷广告创意舞台,轻松闯荡财富之海!

本文由 ChatMoney团队出品 引言 在广告创意行业,创新和高效是赢得市场的关键。而我今天要分享的就是如何利用ChatmoneyAI这款强大的人工智能工具,打破创新难题,赚取丰厚收益。 让我告诉你一个小秘密,有客户曾在一个月内&#xf…

git merge(3个模式) 与 git rebase 图文详解区别

目录 1 git merge1.1 模式一:fast-forward(–ff)1.2 模式二:non-Fast-forward(–no-ff)1.3 模式三:fast-forward only(–ff-only) 2 git rebase3 区别 1 git merge git merge有好几种不同的模式 默认情况下你直接使用 git merge 命令&#x…

从boost库到时间戳

一、以问题引入 授权证书一般有到期时间的说法,公司测试同事在测试更新后的证书时,将系统时间调到了2050年,重启服务后发现各个进程的cpu占用率特别高;结合日志分析,发现这些进程 都在不停的刷heartbeat()的日志&#…

C++17并行算法与HIPSTDPAR

C17 parallel algorithms and HIPSTDPAR — ROCm Blogs (amd.com) C17标准在原有的C标准库中引入了并行算法的概念。像std::transform这样的并行版本算法保持了与常规串行版本相同的签名,只是增加了一个额外的参数来指定使用的执行策略。这种灵活性使得已经使用C标准…

AI 音乐大模型:创新的曙光还是创意产业的阴影?

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

[面试题]Kafka

[面试题]Java【基础】[面试题]Java【虚拟机】[面试题]Java【并发】[面试题]Java【集合】[面试题]MySQL[面试题]Maven[面试题]Spring Boot[面试题]Spring Cloud[面试题]Spring MVC[面试题]Spring[面试题]MyBatis[面试题]Nginx[面试题]缓存[面试题]Redis[面试题]消息队列[面试题]…

RAG(检索增强生成)的演变:初级 RAG、高级 RAG 和模块化 RAG 架构

大型语言模型(LLMs)通过在自然语言任务及其它领域的成功应用,如 ChatGPT、Bard、Claude 等所示,已经彻底改变了 AI 领域。这些 LLMs 能够生成从创意写作到复杂代码的文本。然而,LLMs 面临着幻觉、过时知识和不透明、无…

基于ChatGPT的大型语言模型试用心得

近年来,ChatGPT这样的大型语言模型,它如同一颗冉冉升起的新星,迅速在商业、教育、娱乐等多个领域照亮了创新的天空,极大地革新了我们的工作与日常生活。 最近我发现一些国内用户也能自由访问的中文ChatGPT APP。这个平台不仅提供…

10招教你玩转Python循环优化

更多Python学习内容:ipengtao.com 在Python编程中,循环是最常见的控制结构之一。尽管Python的循环语法简单明了,但在处理大量数据或进行复杂计算时,循环可能会成为性能瓶颈。本文将介绍10种加速Python循环的方法,帮助在…

[Linux] 系统的基本架构特点

Linux系统的基本结构 Linux is also a subversion of UNIX,it follows the basic structure of UNIX 内核(kernel): 操作系统的基本部分 管理与硬件相关的功能,分模块进行 常驻模块:进程控制IO操作文件\磁盘访问 用户不能直接访问内核 外壳(s…

数据资产:打破数据孤岛,实现数据互联互通,构建企业智能化转型的重要桥梁。通过高效整合与利用数据资源,推动企业决策的科学化、精准化,助力企业迈向智能化新时代

目录 一、引言 二、数据孤岛现象及其影响 三、打破数据孤岛,实现数据互联互通 1、制定统一的数据标准和管理规范 2、建设统一的数据平台 3、推广数据共享和开放文化 四、数据资产在智能化转型中的重要作用 1、推动企业决策的科学化、精准化 2、优化企业运营…

盘点下常见 HDFS JournalNode 异常的问题原因和修复方法

盘点下常见 HDFS JournalNode 异常的问题原因和修复方法 最近在多个客户现场以及公司内部环境,都遇到了因为 JournalNode 异常导致 HDFS 服务不可用的问题,在此总结下相关知识。 1 HDFS HA 高可用和 JournalNode 概述 HDFS namenode 有 SPOF 单点故障…

【尚庭公寓SpringBoot + Vue 项目实战】移动端项目初始化(十九)

【尚庭公寓SpringBoot Vue 项目实战】移动端项目初始化(十九) 文章目录 【尚庭公寓SpringBoot Vue 项目实战】移动端项目初始化(十九)1、 SpringBoot配置2、Mybatis-Plus配置3、Knife4j配置4、导入基础代码5、导入接口定义代码6…