首页 > 其他分享 >聊一聊 Monitor.Wait 和 Pluse 的底层玩法

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

时间:2024-06-20 11:54:01浏览次数:23  
标签:SyncBlock Monitor LinkSB Pluse 聊一聊 WaitEventLink PTR Wait

一:背景

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 会有一个新的体会。


图片名称

标签:SyncBlock,Monitor,LinkSB,Pluse,聊一聊,WaitEventLink,PTR,Wait
From: https://www.cnblogs.com/huangxincheng/p/18258390

相关文章

  • 自动驾驶 Apollo 源码分析:ProcessMonitor
    自动驾驶 Apollo 源码分析:ProcessMonitor本篇文章分析 Apollo 中监控模块中监控进程状态的相关代码。附赠自动驾驶最全的学习资料和量产经验:链接1. ProcessMonitorProcessMonitor 是一个普通的定时器组件,内部函数也只是常规的 RunOnce 和 UpdateStatus,所以,......
  • 【Android面试八股文】我们来聊一聊IdelHandler吧,IdelHandler能干什么?怎么使用?有什么
    文章目录一、简单说说Handler机制二、IdleHandler是什么?怎么用?三、什么时候出现空闲时间可以执行IdleHandler四、IdleHander是如何保证不进入死循环的?五、你知道在Framework中如何使用IdleHander?六、一些其他面试问题Handler机制算是Android基本功,面试......
  • 今天我们来聊一聊视频号小店的优点和缺点~
    大家好,我是喷火龙。话不多说,直接进入正题。先来讲讲视频号小店平台发展问题与项目优势:1,关于视频号:视频号本质是微信体系里面的短视频平台,依托着微信庞大的流量和用户所展开的短视频和直播内容的对外输出。在2023微信公开课PRO上,视频号团队介绍,现阶段用户使用时长已经超过了......
  • Process Monitor 应用 高级技巧和应用场景,从而更加灵活、高效地利用这个强大的系统监
    ProcessMonitor初级应用的大纲:1.简介与基本概念介绍ProcessMonitor的作用和功能。解释进程监视器中的基本概念,如进程、线程、事件等。2.安装与配置指导用户如何下载和安装ProcessMonitor。演示如何配置进程监视器以满足用户需求,包括过滤器和列设置。3.进程监视与......
  • k8s servicemonitor 采集超时配置
    背景说明我们有时候在编写exporter时,其中某个采集的metrics接口获取数据很慢,可能需要达到10~20S,基于此种情况,如果我们按照ServiceMonitor默认的配置进行,这里默认scrapeTimeout为10S,采集时会超时,对应Prometheus的Target会提示,servicemonitor对应的State状态为Down。这里我们需要修......
  • Nginx R31 doc-15-Live Activity Monitoring 实时活动监控
    前言大家好,我是老马。很高兴遇到你。我们为java开发者实现了java版本的nginxhttps://github.com/houbb/nginx4j如果你想知道servlet如何处理的,可以参考我的另一个项目:手写从零实现简易版tomcatminicat手写nginx系列如果你对nginx原理感兴趣,可以读一下从......
  • 聊一聊抖音小店
    大家好,我是喷火龙。一招鲜,吃遍天,以不变,应万变;这是上次直播分享结束的时候我做的一个小总结,其实这几个字,也可以作为我们这几年电商经历的一个缩影与概括,也是对我们这群人最真切的一种形容。我说过电商这件事我们还要做很多很多年,我不在意别人怎么看,也不想过分思考往后那些复......
  • 2.3.3 加入monitor
    作用:监测DUT的行为,driver负责把transaction级别的数据转变成DUT的端口级别,并驱动给DUT,monitor的行为与其相对,用于收集DUT的端口数据,并将其转换成transaction交给后续的组件如referencemodel,scoreboard等处理。一些代码中用到的方法如下:monitor代码如下:需要注意1.所有的......
  • Nginx-01-聊一聊 nginx
    nginx系列Nginx-01-聊一聊nginxNginx-01-Nginx是什么Nginx-02-为什么使用NginxNginx-02-NginxUbuntu安装+windows10+WSLubuntu安装nginx实战笔记Nginx-02-基本使用Nginx-03-Nginx项目架构Nginx-04-DockerNginxNginx-05-nginx反向代理是什么?windows下如......
  • 聊一聊关于线程池的那些事情
     在现代软件开发中,线程池是一种广泛应用的并发执行模式,尤其在处理大量短暂异步任务的场景中,线程池能够提高程序性能,减少资源消耗。本文将深入探讨Java线程池的工作原理,包括其核心参数、执行过程以及应用场景等方面。一、线程池的核心参数Java线程池在java.util.concurrent包下......