一:背景
1. 講故事
上週看了一位訓練營朋友的dump,據朋友說他的程式卡死了,看完之後發現是一例經典的死鎖問題,蠻有意思,這個案例算是學習 .NET高階除錯
入門級的案例,這裡和大家分享一下。
二:WinDbg 分析
1. 程式為什麼會卡死
因為是窗體程式,所以看主執行緒的執行緒棧就好了,如果卡在 使用者態
那這個問題相對容易解決,如果卡在 核心態
這個問題就比較複雜了,需要開啟 WinDbg 的本機核心除錯或者雙機除錯才能找到最終的問題。
既然已經說了是入門級,那肯定是卡在 使用者態
層面啦,我們用 !clrstack
命令觀察下主執行緒的執行緒棧即可,輸出如下:
0:000:x86> !clrstack
OS Thread Id: 0x31d8 (0)
Child SP IP Call Site
00f9ec28 00e9e108 [GCFrame: 00f9ec28]
00f9ed08 00e9e108 [GCFrame: 00f9ed08]
00f9ed24 00e9e108 [HelperMethodFrame_1OBJ: 00f9ed24] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
00f9eda0 70c08468 System.Threading.Monitor.Enter(System.Object, Boolean ByRef) [f:\dd\ndp\clr\src\BCL\system\threading\monitor.cs @ 62]
00f9edb0 0ce916c7 xxxx.GetAlarmCount(xxx)
00f9ee28 0961f41f xxx.xxx()
00f9ef04 0961d60a xxxx.xxx(System.Object, System.EventArgs)
00f9ef50 6de03dc9 System.Windows.Forms.Timer.OnTick(System.EventArgs)
00f9ef58 6de053d9 System.Windows.Forms.Timer+TimerNativeWindow.WndProc(System.Windows.Forms.Message ByRef)
00f9ef64 6ddd38d0 System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)
00f9f1b0 0130d5d4 [InlinedCallFrame: 00f9f1b0]
00f9f1ac 6de375bd DomainBoundILStubClass.IL_STUB_PInvoke(MSG ByRef)
00f9f1b0 6dde44e3 [InlinedCallFrame: 00f9f1b0] System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG ByRef)
00f9f1e4 6dde44e3 System.Windows.Forms.Application+ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr, Int32, Int32)
00f9f1e8 6dde40d1 [InlinedCallFrame: 00f9f1e8]
00f9f270 6dde40d1 System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner(Int32, System.Windows.Forms.ApplicationContext)
00f9f2c0 6dde3f23 System.Windows.Forms.Application+ThreadContext.RunMessageLoop(Int32, System.Windows.Forms.ApplicationContext)
00f9f2ec 6ddbc83d System.Windows.Forms.Application.Run(System.Windows.Forms.Form)
00f9f300 01350a6e CleanControl.Program.Main(System.String[])
00f9f4ec 71d00556 [GCFrame: 00f9f4ec]
...
從卦中看,主執行緒卡在 Monitor.Enter
處,也就表明當前執行緒在 GetAlarmCount()
方法的一個 lock 處等待。
2. 誰在持有鎖
要想找到誰在持有鎖,需要理解 lock
的底層機制,它是建立在 AutoResetEvent + ObjectHeader
基礎之上的一種鎖玩法,在 CLR 層面使用 SyncBlk
的 class 來承載的,參考如下程式碼:
class SyncBlock
{
// ObjHeader creates our Mutex and Event
friend class ObjHeader;
friend class SyncBlockCache;
friend struct ThreadQueue;
#ifdef DACCESS_COMPILE
friend class ClrDataAccess;
#endif
friend class CheckAsmOffsets;
protected:
AwareLock m_Monitor; // the actual monitor
SLink m_Link;
DWORD m_dwHashCode;
WCHAR m_BSTRTrailByte;
}
要想觀察這些 SyncBlk
資訊,可以用 WinDbg 提供的快捷命令 !syncblk
來觀察。
0:000:x86> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
180 0b86e8e8 3 1 01452a08 3728 24 039da140 System.Object
-----------------------------
Total 339
CCW 5
RCW 2
ComClassFactory 0
Free 4
從卦中看,當前持有 lock 的執行緒是 24
號,那這個執行緒為什麼遲遲不退出鎖呢? 這就需要到這個執行緒棧上找原因了, 使用命令 ~24s; !clrstack
即可。
0:004:x86> ~24s
ntdll_779a0000!NtWaitForMultipleObjects+0xc:
77a11b2c c21400 ret 14h
0:024:x86> !clrstack
OS Thread Id: 0x3728 (24)
Child SP IP Call Site
0e99e504 0000002b [HelperMethodFrame_1OBJ: 0e99e504] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
0e99e5e8 70bdd952 System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\waithandle.cs @ 243]
0e99e600 70bdd919 System.Threading.WaitHandle.WaitOne(Int32, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\waithandle.cs @ 194]
0e99e614 6e4aa4a8 System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle)
0e99e654 6e8585af System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)
0e99e658 6e4acc4f [InlinedCallFrame: 0e99e658]
0e99e6e0 6e4acc4f System.Windows.Forms.Control.Invoke(System.Delegate, System.Object[])
...
0e99e83c 0f46512c xxx.AddAlarmQueue(xxx)
...
0e99ea84 0d3f2783 xxx.Func()
0e99ead8 70be2e01 System.Threading.ThreadHelper.ThreadStart_Context(System.Object) [f:\dd\ndp\clr\src\BCL\system\threading\thread.cs @ 74]
...
從卦中看,其中的 MarshaledInvoke
方法很刺眼,它表示工作執行緒透過 Invoke 向主執行緒的控制元件推送資料,因為主執行緒遲遲沒有響應它,導致它一直在等待,而恰恰它又持有了 lock 鎖,不趕巧主執行緒因為獲取lock在遲遲等待又無法響應工作執行緒的 MarshaledInvoke 請求,導致一種死鎖狀態,如果要畫個圖大概是這樣的。
3. 如何化解
尋得化解之法,需要看下程式中是怎麼持有 lock 鎖的,仔細觀察程式碼之後,終於找到了 lock 程式碼處,截圖如下:
對程式碼敏感得朋友相信一眼就能看出,這 lock 的粒度真tmd的大,只要 lock 中有一處呼叫了 Invoke,如果不湊巧主執行緒剛好在等待 lock ,那就死鎖了,正如本篇中的 死鎖。
三:總結
這次卡死事故,本質上來說是程式設計師對鎖
的使用沒有一個好的習慣,沒有遵循鎖的儘早釋放原則。
其實這一塊關係型資料庫做的特別好,鎖的粒度分的很細,諸如:行鎖,RID鎖,Key鎖,頁鎖,表鎖,在必要的時候還會涉及到鎖的升級,將效能,鎖開銷,一致性 做到了極致,非常值得我們研究和學習。