WinForms应用程序因SystemEvents.OnUserPreferenceChanged事件而挂起

4
我一直在应对客户安装中出现的奇怪应用程序挂起问题。 经过尝试了几种方法,我得出了一个结论:如果没有转储数据,无法解决该问题。 因此,我从其中一个客户的卡顿时间获得了一个转储文件。 请注意,这只发生在我的安装中,而不是在我的开发计算机中。
在我的转储文件中,我看到SystemEvents.OnUserPreferenceChanged事件导致我的UI线程阻塞,等待一个不再传递消息的线程。
在这里和谷歌上搜索后,我发现其他一些人也遇到了这个问题。 我反复检查代码,以查看我们是否在非UI线程上创建了控件或表单,但没有找到任何线索。
这是我的!clrstack。
 0012ee5c 7c90e514 [HelperMethodFrame_1OBJ: 0012ee5c] 
System.Threading.WaitHandle.WaitOneNative(Microsoft.Win32.SafeHandles.SafeWaitHandle, UInt32, Boolean, Boolean)
0012ef08 792b68af System.Threading.WaitHandle.WaitOne(Int64, Boolean)
0012ef24 792b6865 System.Threading.WaitHandle.WaitOne(Int32, Boolean)
0012ef38 7b6f1a4f System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle)
0012ef4c 7ba2d68b System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)
0012efec 7b6f33ac System.Windows.Forms.Control.Invoke(System.Delegate, System.Object[])
0012f020 7b920bd7 System.Windows.Forms.WindowsFormsSynchronizationContext.Send(System.Threading.SendOrPostCallback, System.Object)
0012f038 7a92ed62 Microsoft.Win32.SystemEvents+SystemEventInvokeInfo.Invoke(Boolean, System.Object[])
0012f06c 7a92dc8f Microsoft.Win32.SystemEvents.RaiseEvent(Boolean, System.Object, System.Object[])
0012f0b8 7a92e227 Microsoft.Win32.SystemEvents.OnUserPreferenceChanged(Int32, IntPtr, IntPtr)
0012f0d8 7aaa06ec Microsoft.Win32.SystemEvents.WindowProc(IntPtr, Int32, IntPtr, IntPtr)
0012f0dc 003c222c [InlinedCallFrame: 0012f0dc] 
0012f2a0 7b1d8d2e System.Windows.Forms.Application+ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32, Int32, Int32)
0012f33c 7b1d8997 System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner(Int32, System.Windows.Forms.ApplicationContext)
0012f390 7b1d87e1 System.Windows.Forms.Application+ThreadContext.RunMessageLoop(Int32, System.Windows.Forms.ApplicationContext)
0012f3c0 7b195931 System.Windows.Forms.Application.Run(System.Windows.Forms.Form)
0012f3d4 034608b6 InsFocusBI.Presentation.MdiMain.NewMdiMainHandler(System.Object, NewSingleInstanceEventArgs)
0012f3ec 034607ac InsFocusBI.Utilities.SingleInstanceHandler.Start(System.String[], System.String)
0012f42c 0346021a InsFocusBI.Presentation.MdiMain.Run(System.String[])
0012f440 0346019b InsFocusBI.Presentation.MdiMain.Main(System.String[])
0012f688 79e71b4c [GCFrame: 0012f688] 

我认为通过转储,我可以找出在其他线程上创建的控件,并从中得到一些线索。

我尝试使用!dso获取所有堆栈对象:

OS Thread Id: 0x4f0 (0)
ESP/REG  Object   Name
0012ed90 0132e8cc System.Windows.Forms.WindowsFormsSynchronizationContext
0012ee1c 06bfe2a0 System.Threading.ManualResetEvent
0012ee30 06bfe2a0 System.Threading.ManualResetEvent
0012ee9c 06bfe2a0 System.Threading.ManualResetEvent
0012eea4 0132381c System.Collections.Hashtable
0012eeb0 06bfe2a0 System.Threading.ManualResetEvent
0012eee0 06bfe2b8 Microsoft.Win32.SafeHandles.SafeWaitHandle
0012ef28 06bfe2a0 System.Threading.ManualResetEvent
0012ef78 06b4d080 System.Windows.Forms.PropertyStore
0012ef80 06b4d018 System.Windows.Forms.Application+MarshalingControl
0012ef88 06b4d018 System.Windows.Forms.Application+MarshalingControl
0012ef8c 06bfe1b0 System.Windows.Forms.Control+ThreadMethodEntry
0012ef90 06b4d018 System.Windows.Forms.Application+MarshalingControl
0012ef94 06b4d018 System.Windows.Forms.Application+MarshalingControl
0012ef9c 06b4d018 System.Windows.Forms.Application+MarshalingControl
0012efa4 06b4d018 System.Windows.Forms.Application+MarshalingControl
0012efd4 06b4d018 System.Windows.Forms.Application+MarshalingControl
0012efe4 06bfe124 System.Object[]    (System.Object[])
0012efe8 06bfe104 System.Threading.SendOrPostCallback
0012efec 06bfe138 System.Windows.Forms.Control+MultithreadSafeCallScope
0012f004 064a8228 System.Threading.Thread
0012f00c 06b4d018 System.Windows.Forms.Application+MarshalingControl
0012f01c 06bfe124 System.Object[]    (System.Object[])
0012f028 06b4cf64 System.Windows.Forms.WindowsFormsSynchronizationContext
0012f034 06bfddc4 System.Object[]    (System.Object[])
0012f038 06b6096c Microsoft.Win32.SystemEvents+SystemEventInvokeInfo
0012f068 06bfddc4 System.Object[]    (System.Object[])
0012f074 0132a174 System.Object
0012f078 06bfdde8 System.Object[]    (Microsoft.Win32.SystemEvents+SystemEventInvokeInfo[])
0012f07c 0132a298 System.Object
0012f0a4 0132a3c4 Microsoft.Win32.SystemEvents
0012f0a8 0132a298 System.Object
0012f0b4 06bfddc4 System.Object[]    (System.Object[])
0012f0c0 0132a3c4 Microsoft.Win32.SystemEvents
0012f270 017dbd10 InsFocusBI.Presentation.Controls.CustomListView
0012f288 0132e8f0 System.Windows.Forms.Application+ThreadContext
0012f2cc 017860c0 System.Windows.Forms.NativeMethods+MSG[]
0012f2d0 0132e8f0 System.Windows.Forms.Application+ThreadContext
0012f2d8 01372050 System.Windows.Forms.Application+ComponentManager
0012f350 0132e8f0 System.Windows.Forms.Application+ThreadContext
0012f38c 01785a74 System.Windows.Forms.ApplicationContext
0012f428 012fd464 System.String    el02
0012f6f4 012f913c System.Object[]    (System.String[])

我看到的唯一东西是InsFocusBI.Presentation.Controls.CustomListView,但我没有看到它在另一个线程上创建。
有什么建议吗?有人能给出另一个想法或尝试的建议吗?
谢谢。
5个回答

7

因为SystemEvents.OnUserPreferenceChanged而引起的冻结问题是非常普遍的bug,这里有Microsoft的解释和建议如何修复。

以下是您可以调用的函数(在冻结之前或之后)以查找哪些特定控件订阅了SystemEvents并在错误的线程上创建,因此可能会冻结您的应用程序:

    private static void CheckSystemEventsHandlersForFreeze()
    {
        var handlers = typeof(SystemEvents).GetField("_handlers", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
        var handlersValues = handlers.GetType().GetProperty("Values").GetValue(handlers);
        foreach (var invokeInfos in (handlersValues as IEnumerable).OfType<object>().ToArray())
        foreach (var invokeInfo in (invokeInfos as IEnumerable).OfType<object>().ToArray())
        {
            var syncContext = invokeInfo.GetType().GetField("_syncContext", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(invokeInfo);
            if (syncContext == null) throw new Exception("syncContext missing");
            if (!(syncContext is WindowsFormsSynchronizationContext)) continue;
            var threadRef = (WeakReference) syncContext.GetType().GetField("destinationThreadRef", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(syncContext);
            if (!threadRef.IsAlive) continue;
            var thread = (Thread)threadRef.Target;
            if (thread.ManagedThreadId == 1) continue;  // Change here if you have more valid UI threads to ignore
            var dlg = (Delegate) invokeInfo.GetType().GetField("_delegate", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(invokeInfo);
            MessageBox.Show($"SystemEvents handler '{dlg.Method.DeclaringType}.{dlg.Method.Name}' could freeze app due to wrong thread: "
                            + $"{thread.ManagedThreadId},{thread.IsThreadPoolThread},{thread.IsAlive},{thread.Name}");
        }
    }

你可能需要注意的是,当你真正需要它的时候,你就不能再点击按钮了 :) - Hans Passant
1
您可以在冻结之前随时通过点击调用此函数。或者,您也可以通过定时器定期调用它,例如每分钟调用一次。如果出现冻结情况,它应该从另一个线程中运行。 - Vlad Rudenko
“坏的” 处理器通常在冻结之前很早就被添加到 SystemEvents 中 :) - Vlad Rudenko
在冻结之前删除。最坏的情况是SystemEvents初始化错误,猜测了错误的UI线程。一个不仅显示简单位图的闪屏可以做到这一点。由于初始化线程不再存在,它现在在线程池线程上触发事件。我不认为你的诊断可以显示出来。 - Hans Passant
1
如果初始化线程不存在了,SystemEvents将不会触发这样的事件,因为它在内部保持对线程的WeakReference,并检查线程的IsAlive属性。我刚测试过并确认了我的应用程序也有这种行为。还测试了即使应用程序挂起,CheckSystemEventsHandlersForFreeze仍然可以从另一个线程正常工作。 - Vlad Rudenko
1
你真是个伟大的人,非常感谢你的帮助!我运行了它并发现问题出在"System.Windows.Forms.RichTextBox"上。我正在创建它以在应用程序角落生成RTF。一旦它在非UI线程中执行...系统就会在下一个系统事件上挂起。 - Piyush Aghera

4

你遇到了一个经典问题,即在非UI线程上控制'创建'的问题。

  • 可能是你正在创建控件。
  • 访问某些属性会在你不知道的情况下创建控件(罕见)。
  • 在创建之前访问句柄属性(可能是间接的)。
  • 被InvokeRequired所迷惑(参考文献1),(参考文献2)。

关于这个主题的绝佳阅读:(参考文献1)(参考文献2)和(参考文献3)。

我不知道如何在转储中找到控件(我自己试过了) 但你可以尝试按照(参考文献4)和(参考文献5)中所述在代码中设置断点。

干杯。

参考文献:

  1. 神秘的挂起或InvokeRequired的伟大欺骗

  2. Control.Trifecta: InvokeRequired, IsHandleCreated和IsDisposed

  3. .NET 2.0 WinForms多线程和一些漫长的日子

  4. 调试UI

  5. 泄漏线程句柄的情况


1

根据Vlad的代码,我找到了一种方法来取消订阅在主UI线程之外订阅的所有对象的系统事件。

这段代码对我很有用,解决了多年来处理系统事件的痛苦:

public static void UnsubscribeSystemEvents()
{
    try
    {
        var handlers = typeof(SystemEvents).GetField("_handlers", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
        var handlersValues = handlers.GetType().GetProperty("Values").GetValue(handlers);
        foreach (var invokeInfos in (handlersValues as IEnumerable).OfType<object>().ToArray())
            foreach (var invokeInfo in (invokeInfos as IEnumerable).OfType<object>().ToArray())
            {
                var syncContext = invokeInfo.GetType().GetField("_syncContext", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(invokeInfo);
                if (syncContext == null) 
                    throw new Exception("syncContext missing");
                if (!(syncContext is WindowsFormsSynchronizationContext))
                    continue;
                var threadRef = (WeakReference)syncContext.GetType().GetField("destinationThreadRef", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(syncContext);
                if (!threadRef.IsAlive)
                    continue;
                var thread = (System.Threading.Thread)threadRef.Target;
                if (thread.ManagedThreadId == 1)
                        continue;  // Change here if you have more valid UI threads to ignore
                var dlg = (Delegate)invokeInfo.GetType().GetField("_delegate", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(invokeInfo);
                var handler = (UserPreferenceChangedEventHandler)Delegate.CreateDelegate(typeof(UserPreferenceChangedEventHandler), dlg.Target, dlg.Method.Name);
                SystemEvents.UserPreferenceChanged -= handler;
            }
    }
    catch ()
    {                
        //trace here your errors
    }
}

0

如果你还不知道如何检查这个问题,可以看一下Kim Greenlee博客并尝试使用Spy++工具解决。也许它会对你有所帮助,我们遇到了同样的问题,并且无法模拟挂起的应用程序问题,但仍在进行大量研究!


0

这并没有回答问题。一旦您拥有足够的声望,您将能够评论任何帖子;相反,提供不需要询问者澄清的答案。- 来自审核 - Shunya
你在说什么?我在链接的帖子中详细解释了问题和解决方案。 - Thanatos
你没有提供答案,只是给出了一个链接到同一个问题中的答案,如果它对你有用,你可以点赞那个答案或者在评论中加上你的修改建议。 - Shunya

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接