在dump分析的过程中经常会看到很多线程卡在Monitor.Wait方法上,曾经也有不少人问我为什么用 !syncblk 看不到 Monitor.Wait 上的锁信息,刚好昨天有时间我就来研究一下。
Monitor.Wait
!syncblk
为了方便讲述,先上一段演示代码,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 { }
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);
Console.WriteLine($"{DateTime.Now} 3. worker2 的逻辑执行完毕...");
Monitor.Pulse(lockObject);
public class Person { }
有了代码和输出之后,接下来就是分析底层玩法了。
研究来研究去总得有个结果,千言万语绘成一张图,截图如下:
从图中可以看到这地方会涉及到一个核心的数据结构 WaitEventLink,参考如下:
WaitEventLink
// Used inside Thread class to chain all events that a thread is waiting for by Object::Waitstruct 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.};
// 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.
};
代码里对每一个字段都做了表述,还是非常清楚的,也看到了这里存在两个队列。
首先我们看下C#的 Monitor.Wait(lockObject) 底层是如何实现的,它对应着 coreclr 的 ObjectNative::WaitTimeout 方法,核心实现如下:
Monitor.Wait(lockObject)
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;}
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;
代码逻辑非常简单,大概步骤如下:
inline PTR_WaitEventLink ThreadQueue::WaitEventLinkForLink(PTR_SLink pLink){ LIMITED_METHOD_CONTRACT; SUPPORTS_DAC; return (PTR_WaitEventLink) (((PTR_BYTE) pLink) - offsetof(WaitEventLink, m_LinkSB));}
inline PTR_WaitEventLink ThreadQueue::WaitEventLinkForLink(PTR_SLink pLink)
LIMITED_METHOD_CONTRACT;
SUPPORTS_DAC;
return (PTR_WaitEventLink) (((PTR_BYTE) pLink) - offsetof(WaitEventLink, m_LinkSB));
LONG LeaveMonitorCompletely(){ return m_Monitor.LeaveCompletely();}void Signal(){ m_SemEvent.SetMonitorEvent();}void CLREventBase::SetMonitorEvent(){ Set();}
LONG LeaveMonitorCompletely()
return m_Monitor.LeaveCompletely();
void Signal()
m_SemEvent.SetMonitorEvent();
void CLREventBase::SetMonitorEvent(){
Set();
总而言之,Monitor.Wait 主要还是用来将Node追加到两大队列,接下来研究下 Monitor.Pulse 的内部实现,这个就比较简单了,无非就是在 m_LinkSB 指向的队列中提取一个Node而已,核心代码如下:
Monitor.Pulse
m_LinkSB
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;}
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;
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
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 都唤醒呢?哈哈,真聪明,源码如下:
Monitor.PulseAll
void SyncBlock::PulseAll(){ WaitEventLink* pWaitEventLink; while ((pWaitEventLink = ThreadQueue::DequeueThread(this)) != NULL) pWaitEventLink->m_EventWait->Set();}
void SyncBlock::PulseAll()
while ((pWaitEventLink = ThreadQueue::DequeueThread(this)) != NULL)
眼尖的朋友会有一个疑问,这个队列数据提取了,那另一个队列的数据是不是也要相应的改动,这个确实,它的逻辑是在Wait方法的 PendingSync syncState(walk); 析构函数里,感兴趣的朋友可以看一下内部的void Restore(BOOL bRemoveFromSB) 方法即可。
void Restore(BOOL bRemoveFromSB)
花了半天研究这东西还是挺有意思的,重点还是要理解下那张图,理解了之后我相信你对 Monitor.Pluse 方法注释中所指的 waiting queue 会有一个新的体会。
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