WindowsFormsSynchronizationContext和System.Events.UserPferenceChanged导致的用户界面冻结

UI Freeze caused by WindowsFormsSynchronizationContext and System.Events.UserPreferenceChanged(WindowsFormsSynchronizationContext和System.Events.UserPferenceChanged导致的用户界面冻结)
本文介绍了WindowsFormsSynchronizationContext和System.Events.UserPferenceChanged导致的用户界面冻结的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我现在已经花了几天时间找到一个冻结我的公司应用程序的错误。可怕的用户首选项更改的用户界面冻结。这不是一个复杂的错误,但在相当大的应用程序中很难找到。有相当多的文章是关于这个错误是如何展开的,但没有关于如何指出错误代码的文章。我已经组合了一个解决方案,以来自多个较旧票证的日志机制的形式,并且(我希望)在它们的基础上做了一些改进。希望它能为解决此问题的下一位程序员节省一些时间。

如何识别错误?

应用程序完全冻结。只需创建一个内存转储,然后通过TaskManager将其关闭。如果您在VisualStudio或WinDbg中打开DMP文件,您可能会看到如下所示的堆栈跟踪

WaitHandle.InternalWaitOne
WaitHandle.WaitOne
Control.WaitForWaitHandle
Control.MarshaledInvoke
Control.Invoke
WindowsFormsSynchronizationContext.Send
System.EventInvokeInfo.Invoke
SystemEvents.RaiseEvent
SystemEvents.OnUserPreferenceChanged
SystemEvents.WindowProc
:
此处重要的两行是OnUserPferenceChanged和";WindowsFormsSynchronizationContext.Send";

原因是什么?

SynchronizationContext是在.NET2中引入的,用于推广线程同步。它为我们提供了像BeginInvoke这样的方法。

UserPferenceChanged事件不言而喻。它将由用户更改其背景、登录或注销、更改Windows强调色和许多其他操作触发。

如果在后台线程上创建一个GUI控件,则在该线程上安装WindowsFormsSynchronizationContext。某些图形用户界面控件在创建或使用某些方法时订阅UserPferenceChanged事件。如果该事件是由用户触发的,则主线程向所有订阅者发送一条消息并等待。在所描述的场景中:没有消息循环的工作线程!应用程序已冻结。

要找到冻结的原因可能特别困难,因为错误的原因(在后台线程上创建图形用户界面元素)和错误状态(应用程序冻结)可能相隔几分钟。有关更多细节和略有不同的场景,请参阅这篇非常好的文章。https://www.ikriv.com/dev/dotnet/MysteriousHang

示例

如何才能出于测试目的引发此错误?

示例1

private void button_Click(object sender, EventArgs e)
{
    new Thread(DoStuff).Start();
}

private void DoStuff()
{
    using (var r = new RichTextBox())
    {
        IntPtr p = r.Handle; //do something with the control
    }

    Thread.Sleep(5000); //simulate some work
}

不错,但也不是很好。如果UserPferenceChanged事件在您使用RichTextBox的几毫秒内被触发,您的应用程序将冻结。有可能发生,但可能性不大。

示例2

private void button_Click(object sender, EventArgs e)
{
    new Thread(DoStuff).Start();
}

private void DoStuff()
{
    var r = new RichTextBox();
    IntPtr p = r.Handle; //do something with the control

    Thread.Sleep(5000); //simulate some work
}

这很糟糕。WindowsFormsSynchronizationContext未被清除,因为RichTextBox未被释放。如果在线程活动时发生UserPferenceChangedEvent,则您的应用程序将冻结。

示例3

private void button_Click(object sender, EventArgs e)
{
    Task.Run(() => DoStuff());
}

private void DoStuff()
{
    var r = new RichTextBox();
    IntPtr p = r.Handle; //do something with the control
}

这是一场噩梦。任务。运行(..)将在线程池的后台线程上执行工作。WindowsFormsSynchronizationContext未被清除,因为RichTextBox未被释放。不清理线程池线程。这个后台线程现在潜伏在您的线程池中,等待UserPferenceChanged事件在您的任务返回后很长一段时间内冻结您的应用程序!

结论:当你知道自己该做什么时,风险是可控的。但只要有可能:避免在后台线程中使用图形用户界面元素!

如何处理此错误?

推荐答案

我从较旧的票证中组合了一个解决方案。非常感谢那些家伙!

WinForms application hang due to SystemEvents.OnUserPreferenceChanged event

https://codereview.stackexchange.com/questions/167013/detecting-ui-thread-hanging-and-logging-stacktrace

此解决方案启动一个新线程,该线程不断尝试检测订阅OnUserPferenceChanged事件的任何线程,然后提供一个调用堆栈来告诉您原因。

public MainForm()
{
    InitializeComponent();

    new Thread(Observe).Start();
}

private void Observe()
{
    new PreferenceChangedObserver().Run();
}


internal sealed class PreferenceChangedObserver
{
    private readonly string _logFilePath = $"filePath\FreezeLog.txt"; //put a better file path here

    private BindingFlags _flagsStatic = BindingFlags.NonPublic | BindingFlags.Static;
    private BindingFlags _flagsInstance = BindingFlags.NonPublic | BindingFlags.Instance;

    public void Run() => CheckSystemEventsHandlersForFreeze();

    private void CheckSystemEventsHandlersForFreeze()
    {
        while (true)
        {
            try
            {
                foreach (var info in GetPossiblyBlockingEventHandlers())
                {
                    var msg = $"SystemEvents handler '{info.EventHandlerDelegate.Method.DeclaringType}.{info.EventHandlerDelegate.Method.Name}' could freeze app due to wrong thread. ThreadId: {info.Thread.ManagedThreadId}, IsThreadPoolThread:{info.Thread.IsThreadPoolThread}, IsAlive:{info.Thread.IsAlive}, ThreadName:{info.Thread.Name}{Environment.NewLine}{info.StackTrace}{Environment.NewLine}";
                    File.AppendAllText(_logFilePath, DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss") + $": {msg}{Environment.NewLine}");
                }
            }
            catch { }
        }
    }

    private IEnumerable<EventHandlerInfo> GetPossiblyBlockingEventHandlers()
    {
        var handlers = typeof(SystemEvents).GetField("_handlers", _flagsStatic).GetValue(null);

        if (!(handlers?.GetType().GetProperty("Values").GetValue(handlers) is IEnumerable handlersValues))
            yield break;

        foreach(var systemInvokeInfo in handlersValues.Cast<IEnumerable>().SelectMany(x => x.OfType<object>()).ToList())
        {
            var syncContext = systemInvokeInfo.GetType().GetField("_syncContext", _flagsInstance).GetValue(systemInvokeInfo);

            //Make sure its the problematic type
            if (!(syncContext is WindowsFormsSynchronizationContext wfsc))
                continue;

            //Get the thread
            var threadRef = (WeakReference)syncContext.GetType().GetField("destinationThreadRef", _flagsInstance).GetValue(syncContext);
            if (!threadRef.IsAlive)
                continue;

            var thread = (Thread)threadRef.Target;
            if (thread.ManagedThreadId == 1) //UI thread
                continue;

            if (thread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId)
                continue;

            //Get the event delegate
            var eventHandlerDelegate = (Delegate)systemInvokeInfo.GetType().GetField("_delegate", _flagsInstance).GetValue(systemInvokeInfo);

            //Get the threads call stack
            string callStack = string.Empty;
            try
            {
                if (thread.IsAlive)
                    callStack = GetStackTrace(thread)?.ToString().Trim();
            }
            catch { }

            yield return new EventHandlerInfo
            {
                Thread = thread,
                EventHandlerDelegate = eventHandlerDelegate,
                StackTrace = callStack,
            };
        }
    }

    private static StackTrace GetStackTrace(Thread targetThread)
    {
        using (ManualResetEvent fallbackThreadReady = new ManualResetEvent(false), exitedSafely = new ManualResetEvent(false))
        {
            Thread fallbackThread = new Thread(delegate () {
                fallbackThreadReady.Set();
                while (!exitedSafely.WaitOne(200))
                {
                    try
                    {
                        targetThread.Resume();
                    }
                    catch (Exception) {/*Whatever happens, do never stop to resume the target-thread regularly until the main-thread has exited safely.*
本站部分内容来源互联网,如果有图片或者内容侵犯您的权益请联系我们删除!

相关文档推荐

DispatcherQueue null when trying to update Ui property in ViewModel(尝试更新ViewModel中的Ui属性时DispatcherQueue为空)
Drawing over all windows on multiple monitors(在多个监视器上绘制所有窗口)
Programmatically show the desktop(以编程方式显示桌面)
c# Generic Setlt;Tgt; implementation to access objects by type(按类型访问对象的C#泛型集实现)
InvalidOperationException When using Context Injection in ASP.Net Core(在ASP.NET核心中使用上下文注入时发生InvalidOperationException)
LINQ many-to-many relationship, how to write a correct WHERE clause?(LINQ多对多关系,如何写一个正确的WHERE子句?)