聊一聊 Monitor.Wait 和 Pulse 的底層玩法

来源:https://www.cnblogs.com/huangxincheng/p/18258390
-Advertisement-
Play Games

一:背景 1. 講故事 在dump分析的過程中經常會看到很多線程卡在Monitor.Wait方法上,曾經也有不少人問我為什麼用 !syncblk 看不到 Monitor.Wait 上的鎖信息,剛好昨天有時間我就來研究一下。 二:Monitor.Wait 底層怎麼玩的 1. 案例演示 為了方便講述,先 ...


一:背景

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 會有一個新的體會。


圖片名稱


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • pip install --upgrade langchain==0.0.279 -i https://pypi.org/simple 1 創建一個LLM 自有算力平臺+開源大模型(需要有龐大的GPU資源)企業自己訓練數據 第三方大模型API(openai/百度文心/阿裡通義千問...)數據無所謂 ...
  • LLM大模型與AI應用的粘合劑。 1 langchain是什麼以及發展過程 LangChain是一個開源框架,旨在簡化使用大型語言模型構建端到端應用程式的過程,也是ReAct(reason+act)論文的落地實現。 2022年10月25日開源 54K+ star 種子輪一周1000萬美金,A輪250 ...
  • 1.創建一個文件夾HCJV_01 2.vscode打開該文件夾,打開終端。 3.使用vite安裝,選擇vue,選擇JavaScript,項目名稱demo01 cnpm create vite@latest 4.跳轉demo01目錄下 cd demo01 5.安裝cnpm cnpm install 嘗 ...
  • 下麵將完成的展示,使用MAPI介面操作Outlook完成通訊錄更新。 using Microsoft.Office.Interop.Outlook; using Microsoft.VisualBasic; using System; using System.Collections; using ...
  • 前言 之前已經分享過幾篇關於中台項目框架的文章,相關介紹就不再贅述 所謂工欲善其事必先利其器,一個項目擁有一個代碼生成器是很有必要的,能夠大大的節省時間,減少手誤,提供開發效率(ps:特別小團隊搞微服務但是沒有代碼生成器,簡直要了老命) 本文將分享如何在中台框架項目 Admin.Core 中添加代碼 ...
  • 官網:Git for Windows 點擊下載安裝。 右擊滑鼠會出現GUI和Bash 選擇git bash here 配置全局用戶名和郵箱(gitee) git config --global user.name "你的名字" git config --global user.email 你的郵箱 ...
  • C# 13 即 .Net 9 按照計劃會在2024年11月發佈,目前一些新特性已經定型,今天讓我們來預覽一個比較大型比較重要的新特性: 擴展類型 extension types ...
  • 目錄前言學習參考過程總結: 前言 做個自由仔。 學習參考 ChatGpt; https://www.cnblogs.com/zhili/p/DesignPatternSummery.html(大佬的,看了好多次) 過程 原由: 一開始只是想查查鏈式調用原理,以為是要繼承什麼介面,實現什麼方法才可以實 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...