翼度科技»论坛 编程开发 .net 查看内容

记一次 .NET 某设备监控系统 死锁分析

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
一:背景

1. 讲故事

上周看了一位训练营朋友的dump,据朋友说他的程序卡死了,看完之后发现是一例经典的死锁问题,蛮有意思,这个案例算是学习 .NET高级调试 入门级的案例,这里和大家分享一下。
二:WinDbg 分析

1. 程序为什么会卡死

因为是窗体程序,所以看主线程的线程栈就好了,如果卡在 用户态 那这个问题相对容易解决,如果卡在 内核态 这个问题就比较复杂了,需要开启 WinDbg 的本机内核调试或者双机调试才能找到最终的问题。
既然已经说了是入门级,那肯定是卡在 用户态 层面啦,我们用 !clrstack 命令观察下主线程的线程栈即可,输出如下:
  1. 0:000:x86> !clrstack
  2. OS Thread Id: 0x31d8 (0)
  3. Child SP       IP Call Site
  4. 00f9ec28 00e9e108 [GCFrame: 00f9ec28]
  5. 00f9ed08 00e9e108 [GCFrame: 00f9ed08]
  6. 00f9ed24 00e9e108 [HelperMethodFrame_1OBJ: 00f9ed24] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
  7. 00f9eda0 70c08468 System.Threading.Monitor.Enter(System.Object, Boolean ByRef) [f:\dd\ndp\clr\src\BCL\system\threading\monitor.cs @ 62]
  8. 00f9edb0 0ce916c7 xxxx.GetAlarmCount(xxx)
  9. 00f9ee28 0961f41f xxx.xxx()
  10. 00f9ef04 0961d60a xxxx.xxx(System.Object, System.EventArgs)
  11. 00f9ef50 6de03dc9 System.Windows.Forms.Timer.OnTick(System.EventArgs)
  12. 00f9ef58 6de053d9 System.Windows.Forms.Timer+TimerNativeWindow.WndProc(System.Windows.Forms.Message ByRef)
  13. 00f9ef64 6ddd38d0 System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)
  14. 00f9f1b0 0130d5d4 [InlinedCallFrame: 00f9f1b0]
  15. 00f9f1ac 6de375bd DomainBoundILStubClass.IL_STUB_PInvoke(MSG ByRef)
  16. 00f9f1b0 6dde44e3 [InlinedCallFrame: 00f9f1b0] System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG ByRef)
  17. 00f9f1e4 6dde44e3 System.Windows.Forms.Application+ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr, Int32, Int32)
  18. 00f9f1e8 6dde40d1 [InlinedCallFrame: 00f9f1e8]
  19. 00f9f270 6dde40d1 System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner(Int32, System.Windows.Forms.ApplicationContext)
  20. 00f9f2c0 6dde3f23 System.Windows.Forms.Application+ThreadContext.RunMessageLoop(Int32, System.Windows.Forms.ApplicationContext)
  21. 00f9f2ec 6ddbc83d System.Windows.Forms.Application.Run(System.Windows.Forms.Form)
  22. 00f9f300 01350a6e CleanControl.Program.Main(System.String[])
  23. 00f9f4ec 71d00556 [GCFrame: 00f9f4ec]
  24. ...
复制代码
从卦中看,主线程卡在 Monitor.Enter 处,也就表明当前线程在 GetAlarmCount() 方法的一个 lock 处等待。
2. 谁在持有锁

要想找到谁在持有锁,需要理解 lock 的底层机制,它是建立在 AutoResetEvent + ObjectHeader 基础之上的一种锁玩法,在 CLR 层面使用 SyncBlk 的 class 来承载的,参考如下代码:
  1. class SyncBlock
  2. {
  3.         // ObjHeader creates our Mutex and Event
  4.         friend class ObjHeader;
  5.         friend class SyncBlockCache;
  6.         friend struct ThreadQueue;
  7. #ifdef DACCESS_COMPILE
  8.         friend class ClrDataAccess;
  9. #endif
  10.         friend class CheckAsmOffsets;
  11. protected:
  12.         AwareLock  m_Monitor;                    // the actual monitor
  13.         SLink       m_Link;
  14.         DWORD m_dwHashCode;
  15.         WCHAR m_BSTRTrailByte;
  16. }
复制代码
要想观察这些 SyncBlk 信息,可以用 WinDbg 提供的快捷命令 !syncblk 来观察。
  1. 0:000:x86> !syncblk
  2. Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
  3.   180 0b86e8e8            3         1 01452a08 3728  24   039da140 System.Object
  4. -----------------------------
  5. Total           339
  6. CCW             5
  7. RCW             2
  8. ComClassFactory 0
  9. Free            4
复制代码
从卦中看,当前持有 lock 的线程是 24 号,那这个线程为什么迟迟不退出锁呢? 这就需要到这个线程栈上找原因了, 使用命令 ~24s; !clrstack 即可。
  1. 0:004:x86> ~24s
  2. ntdll_779a0000!NtWaitForMultipleObjects+0xc:
  3. 77a11b2c c21400          ret     14h
  4. 0:024:x86> !clrstack
  5. OS Thread Id: 0x3728 (24)
  6. Child SP       IP Call Site
  7. 0e99e504 0000002b [HelperMethodFrame_1OBJ: 0e99e504] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
  8. 0e99e5e8 70bdd952 System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\waithandle.cs @ 243]
  9. 0e99e600 70bdd919 System.Threading.WaitHandle.WaitOne(Int32, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\waithandle.cs @ 194]
  10. 0e99e614 6e4aa4a8 System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle)
  11. 0e99e654 6e8585af System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)
  12. 0e99e658 6e4acc4f [InlinedCallFrame: 0e99e658]
  13. 0e99e6e0 6e4acc4f System.Windows.Forms.Control.Invoke(System.Delegate, System.Object[])
  14. ...
  15. 0e99e83c 0f46512c xxx.AddAlarmQueue(xxx)
  16. ...
  17. 0e99ea84 0d3f2783 xxx.Func()
  18. 0e99ead8 70be2e01 System.Threading.ThreadHelper.ThreadStart_Context(System.Object) [f:\dd\ndp\clr\src\BCL\system\threading\thread.cs @ 74]
  19. ...
复制代码
从卦中看,其中的 MarshaledInvoke 方法很刺眼,它表示工作线程通过 Invoke 向主线程的控件推送数据,因为主线程迟迟没有响应它,导致它一直在等待,而恰恰它又持有了 lock 锁,不赶巧主线程因为获取lock在迟迟等待又无法响应工作线程的 MarshaledInvoke 请求,导致一种死锁状态,如果要画个图大概是这样的。

3. 如何化解

寻得化解之法,需要看下程序中是怎么持有 lock 锁的,仔细观察代码之后,终于找到了 lock 代码处,截图如下:

对代码敏感得朋友相信一眼就能看出,这 lock 的粒度真tmd的大,只要 lock 中有一处调用了 Invoke,如果不凑巧主线程刚好在等待 lock ,那就死锁了,正如本篇中的 死锁。
三:总结

这次卡死事故,本质上来说是程序员对锁的使用没有一个好的习惯,没有遵循锁的尽早释放原则。
其实这一块关系型数据库做的特别好,锁的粒度分的很细,诸如:行锁,RID锁,Key锁,页锁,表锁,在必要的时候还会涉及到锁的升级,将性能,锁开销,一致性 做到了极致,非常值得我们研究和学习。

来源:https://www.cnblogs.com/huangxincheng/archive/2023/04/06/17292169.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具