问题描述
我现在已经花了几天时间找到一个冻结我的公司应用程序的错误。可怕的用户首选项更改的用户界面冻结。这不是一个复杂的错误,但在相当大的应用程序中很难找到。有相当多的文章是关于这个错误是如何展开的,但没有关于如何指出错误代码的文章。我已经组合了一个解决方案,以来自多个较旧票证的日志机制的形式,并且(我希望)在它们的基础上做了一些改进。希望它能为解决此问题的下一位程序员节省一些时间。
如何识别错误?
应用程序完全冻结。只需创建一个内存转储,然后通过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.*