一个超经典 WinForm 卡死问题的再反思

一:背景1.讲故事【一个超经典 WinForm 卡死问题的再反思】这篇文章起源于昨天的一位朋友发给我的dump文件 , 说它的程序出现了卡死,看了下程序的主线程栈,居然又碰到了 OnUserPreferenceChanged 导致的挂死问题,真的是经典中的经典,线程栈如下:
0:000:x86> !clrstackOS Thread Id: 0x4eb688 (0)Child SPIP Call Site002fed38 0000002b [HelperMethodFrame_1OBJ: 002fed38] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)002fee1c 5cddad21 System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)002fee34 5cddace8 System.Threading.WaitHandle.WaitOne(Int32, Boolean)002fee48 538d876c System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle)002fee88 53c5214a System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)002fee8c 538dab4b [InlinedCallFrame: 002fee8c]002fef14 538dab4b System.Windows.Forms.Control.Invoke(System.Delegate, System.Object[])002fef48 53b03bc6 System.Windows.Forms.WindowsFormsSynchronizationContext.Send(System.Threading.SendOrPostCallback, System.Object)002fef60 5c774708 Microsoft.Win32.SystemEvents+SystemEventInvokeInfo.Invoke(Boolean, System.Object[])002fef94 5c6616ec Microsoft.Win32.SystemEvents.RaiseEvent(Boolean, System.Object, System.Object[])002fefe8 5c660cd4 Microsoft.Win32.SystemEvents.OnUserPreferenceChanged(Int32, IntPtr, IntPtr)002ff008 5c882c98 Microsoft.Win32.SystemEvents.WindowProc(IntPtr, Int32, IntPtr, IntPtr)...说实话,这种dump从去年看到今年,应该不下五次了,都看烦了,其形成原因是:

  • 未在主线程中生成用户控件,导致用 WindowsFormsSynchronizationContext.Send 跨线程封送时,对方无法响应请求进而挂死
虽然知道原因,但有一个非常大的遗憾就是在 dump 中找不到到底是哪一个控件,只能笼统的告诉朋友 , 让其洞察下代码是哪里用了工作线程创建了 用户控件 ,  有些朋友根据这个信息成功的找到,也有朋友因为各种原因没有找到,比较遗憾 。
为了不让这些朋友的遗憾延续下去 , 这一篇做一个系统归纳,希望能助这些朋友一臂之力 。
二:解决方案1. 背景这个问题的形成详情,我在去年的一篇文章为:记一次 .NET 某新能源汽车锂电池检测程序 UI挂死分析 https://www.cnblogs.com/huangxincheng/p/15245554.html 中已经做过分享,因为 dump 中找不到问题的 Control,所以也留下了一些遗憾,这一篇就做个补充 。
2. 问题突破点分析熟悉 WinForm 底层的朋友应该知道 , 一旦在 工作线程 上创建了 Control 控件,框架会自动给这个线程配备一个 WindowsFormsSynchronizationContext 和其底层的 MarshalingControl,这个是有源码支撑的,大家可以找下 Control 的构造函数,简化后的源码如下:
public class Control : Component{internal Control(bool autoInstallSyncContext){//***if (autoInstallSyncContext){WindowsFormsSynchronizationContext.InstallIfNeeded();}}}public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable{private Control controlToSendTo;private WeakReference destinationThreadRef;public WindowsFormsSynchronizationContext(){DestinationThread = Thread.CurrentThread;Application.ThreadContext threadContext = Application.ThreadContext.FromCurrent();if (threadContext != null){controlToSendTo = threadContext.MarshalingControl;}}internal static void InstallIfNeeded(){try{SynchronizationContext synchronizationContext = AsyncOperationManager.SynchronizationContext;if (synchronizationContext == null || synchronizationContext.GetType() == typeof(SynchronizationContext)){AsyncOperationManager.SynchronizationContext = new WindowsFormsSynchronizationContext();}}finally{inSyncContextInstallation = false;}}}public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable{public WindowsFormsSynchronizationContext(){DestinationThread = Thread.CurrentThread;Application.ThreadContext threadContext = Application.ThreadContext.FromCurrent();if (threadContext != null){controlToSendTo = threadContext.MarshalingControl;}}}internal sealed class ThreadContext{internal Control MarshalingControl{get{lock (this){if (marshalingControl == null){marshalingControl = new MarshalingControl();}return marshalingControl;}}}}这段代码可以挖到下面两点信息 。
  1. 一旦 Control 创建在工作线程上,那这个线程就会安装一个 WindowsFormsSynchronizationContext 变量 , 比如此时就存在两个对象了 。
0:000:x86> !dsoOS Thread Id: 0x4eb688 (0)ESP/REGObjectName002FEC40 025a0fb0 System.Windows.Forms.WindowsFormsSynchronizationContext...002FEF44 0260992c System.Object[](System.Object[])002FEF48 02d69164 System.Windows.Forms.WindowsFormsSynchronizationContext...

推荐阅读