WindowsFormsSynchronizationContext和System.Events.UserPferenceChanged导致的用户界面冻结 [英] UI Freeze caused by WindowsFormsSynchronizationContext and System.Events.UserPreferenceChanged

查看:70
本文介绍了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.*/}
                }
            });
            fallbackThread.Name = "GetStackFallbackThread";
            try
            {
                fallbackThread.Start();
                fallbackThreadReady.WaitOne();
                //From here, you have about 200ms to get the stack-trace.
                targetThread.Suspend();
                StackTrace trace = null;
                try
                {
                    trace = new StackTrace(targetThread, true);
                }
                catch (ThreadStateException) { }
                try
                {
                    targetThread.Resume();
                }
                catch (ThreadStateException) {/*Thread is running again already*/}
                return trace;
            }
            finally
            {
                //Just signal the backup-thread to stop.
                exitedSafely.Set();
                //Join the thread to avoid disposing "exited safely" too early. And also make sure that no leftover threads are cluttering iis by accident.
                fallbackThread.Join();
            }
        }
    }

    private class EventHandlerInfo
    {
        public Delegate EventHandlerDelegate { get; set; }
        public Thread Thread { get; set; }
        public string StackTrace { get; set; }
    }
}

注意

这是一次非常丑陋的黑客攻击。它以一种非常侵入性的方式处理线程。它永远不应该看到一个实时的客户系统。我已经很紧张地将其部署到客户测试系统中。

2)如果您得到一个日志文件,它可能会非常大。任何线程都可能导致数百个条目。从最旧的条目开始,修复它,然后重复。(由于示例3中的受污染线程方案,它也可能包含误报)

3)我不确定这次黑客攻击对性能的影响。我以为它会很大。令我惊讶的是,这几乎不值得注意。但在其他系统上可能有所不同

这篇关于WindowsFormsSynchronizationContext和System.Events.UserPferenceChanged导致的用户界面冻结的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆