我花了幾天的時間發現了一個凍結我公司應用程式的錯誤。可怕的 UserPreferenceChanged UI 凍結。這不是一個復雜的錯誤,但在相當大的應用程式中很難找到。有很多關于這個錯誤如何展開的文章,但沒有關于如何將手指放在錯誤代碼上的文章。我已經整理了一個解決方案,以來自多個舊票的日志記錄機制的形式,并且(我希望)對它們進行了一些改進。可能會為下一個遇到此問題的程式員節省一些時間。
如何識別錯誤?
應用程式完全凍結。除了創建記憶體轉儲然后通過 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
:
這里重要的兩行是“OnUserPreferenceChanged”和“WindowsFormsSynchronizationContext.Send”
原因是什么?
SynchronizationContext 是隨 .NET2 一起引入的,用于概括執行緒同步。它為我們提供了諸如“BeginInvoke”之類的方法。
UserPreferenceChanged 事件是不言自明的。它將由用戶更改其背景、登錄或注銷、更改 Windows 強調色和許多其他操作觸發。
如果在后臺執行緒上創建 GUI 控制元件,則會在所述執行緒上安裝 WindowsFormsSynchronizationContext。某些 GUI 控制元件在創建或使用某些方法時訂閱 UserPreferenceChanged 事件。如果這個事件是由用戶觸發的,主執行緒會向所有訂閱者發送一條訊息并等待。在描述的場景中:沒有訊息回圈的作業執行緒!應用程式被凍結。
找到凍結的原因可能特別困難,因為錯誤的原因(在后臺執行緒上創建 GUI 元素)和錯誤狀態(應用程式凍結)可能相隔幾分鐘。有關更多詳細資訊和略有不同的場景,請參閱這篇非常好的文章。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
}
不錯,但也不錯。如果 UserPreferenceChanged 事件在您使用 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 沒有被處理。如果 UserPreferenceChangedEvent 在執行緒存活時發生,您的應用程式將凍結。
示例 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
}
這是一場噩夢。Task.Run(..) 將在執行緒池的后臺執行緒上執行作業。WindowsFormsSynchronizationContext 沒有被清除,因為 RichTextBox 沒有被釋放。執行緒池執行緒不會被清理。這個后臺執行緒現在潛伏在你的執行緒池中,等待 UserPreferenceChanged 事件在你的任務回傳很久之后凍結你的應用程式!
結論:當您知道自己在做什么時,風險是可控的。但只要有可能:避免在后臺執行緒中使用 GUI 元素!
這個bug怎么處理?
uj5u.com熱心網友回復:
我從舊票中整理了一個解決方案。非常感謝那些家伙!
由于 SystemEvents.OnUserPreferenceChanged 事件,WinForms 應用程式掛起
https://codereview.stackexchange.com/questions/167013/detecting-ui-thread-hanging-and-logging-stacktrace
此解決方案啟動一個新執行緒,該執行緒不斷嘗試檢測訂閱 OnUserPreferenceChanged 事件的任何執行緒,然后提供一個呼叫堆疊,告訴您為什么會這樣。
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; }
}
}
注意力
1)這是一個非常丑陋的黑客。它以一種非常侵入性的方式處理執行緒。它永遠不應該看到一個實時的客戶系統。將它部署到客戶測驗系統時,我已經很緊張了。
2)如果你得到一個日志檔案,它可能會很大。任何執行緒都可能導致數百個條目。從最舊的條目開始,修復它并重復。(由于示例 3 中的“受污染執行緒”場景,它也可能包含誤報)
3)我不確定這個黑客對性能的影響。我以為它會很大。令我驚訝的是,它幾乎不引人注目。雖然在其他系統上可能會有所不同
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/391060.html
