一個超經典 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...

推薦閱讀