WPF窗口最小化崩溃、ScrollToEnd触发FailFast错误排查求助
WPF窗口最小化时RichTextBox日志控件导致进程崩溃排查
问题复现场景
- 独立WPF项目开发中,仅通过设置
WindowState属性触发窗口最小化时,程序直接崩溃退出 - 窗口处于最小化状态时调用日志控件的
ScrollToEnd方法,同样会触发崩溃 - 日志模块为自定义
UserControl,常规异常捕获无法拦截报错,进程直接终止
报错堆栈
Framework Version: v4.0.30319 Description: The application requested process termination through System.Environment.FailFast(string message). Message: Unrecoverable system error. at System.Environment.FailFast(System.String) at MS.Internal.Invariant.FailFast(System.String, System.String) at System.Windows.Documents.SplayTreeNode.get_Role() at System.Windows.Documents.SplayTreeNode.Splay() at System.Windows.Documents.SplayTreeNode.GetMinSibling() at System.Windows.Documents.TextContainer.get_FirstContainedNode() at System.Windows.Documents.TextElementCollection`1[[System.__Canon, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]].get_FirstChild() at System.Windows.Documents.TextElementCollection`1[[System.__Canon, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]].get_Count() at Wmanager.Windows.WindowControls.LogView.AppendMessage(System.Windows.Controls.RichTextBox, System.String, System.Drawing.Color) at Wmanager.Windows.WindowControls.LogView+<>c__DisplayClass1_0.<OnWrite>b__0() at System.Threading.Tasks.Task.InnerInvoke() at System.Threading.Tasks.Task.Execute() at System.Threading.Tasks.Task.ExecutionContextCallback(System.Object) at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) at System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef) at System.Threading.Tasks.Task.ExecuteEntry(Boolean) at System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() at System.Threading.ThreadPoolWorkQueue.Dispatch() at System._ThreadPoolWaitCallback.PerformWaitCallback()
问题控件原始实现代码
public LogView() { InitializeComponent(); Logs.Document.Blocks.Remove(Logs.Document.Blocks.FirstBlock); LogEvent.OnWrite += OnWrite; } private void OnWrite(string message, Color col) { try { Task.Run(() => { AppendMessage(Logs, message, col); }); } catch(Exception ex) { Logging.WriteError($"LogView > OnWrite() > {ex}"); } } private static void AppendMessage(RichTextBox textBox, string message, Color col) { lock(MessageLock) { try { if (!textBox.Dispatcher.CheckAccess()) { textBox.Dispatcher.Invoke(new Action<RichTextBox, string, Color>(AppendMessage), new object[] { textBox, message, col }); } else { Paragraph paragraph = new Paragraph(new Run(message)); paragraph.Foreground = new System.Windows.Media.SolidColorBrush(new System.Windows.Media.Color { A = col.A, R = col.R, G = col.G, B = col.B }); textBox.Document.Blocks.Add(paragraph); } int Block = 0; while (Block <= 200 && textBox.Document.Blocks.Count > 300) { textBox.Document.Blocks.Remove(textBox.Document.Blocks.FirstBlock); Block++; } } catch (Exception ex) { Logging.WriteError(ex.ToString()); } } } private static object MessageLock = new object();
根因定位
- 跨线程UI访问逻辑漏洞:当前实现用
Task.Run把日志追加逻辑丢到线程池执行,虽然加了Dispatcher调度判断,但删除旧日志块的逻辑(读取Blocks.Count、删除FirstBlock)写在了Dispatcher判断分支外。非UI线程进入方法时,通过Invoke切回UI线程完成段落添加后,会回到线程池线程继续执行后面的文档访问操作,直接跨线程操作RichTextBox的文档树。WPF的FlowDocument内部用伸展树(SplayTree)实现,遇到跨线程并发访问时会触发内部一致性断言,直接调用FailFast终止进程,这类错误无法被常规try/catch捕获,和堆栈报错位置完全吻合。 - 最小化状态放大并发冲突概率:窗口最小化时WPF会临时挂起部分可视化树渲染、布局计算逻辑,文档树状态切换的窗口期如果被后台线程访问,比窗口正常显示时更容易触发内部状态校验失败,这也是最小化时必现、正常状态下偶现的原因。
- 锁机制无法规避冲突:当前的静态
MessageLock只能锁自定义代码的执行路径,无法限制WPF UI线程本身对文档树的访问,完全挡不住并发访问问题。 - 最小化状态调用ScrollToEnd触发额外布局计算:窗口最小化时调用
ScrollToEnd会强制触发文档树的遍历和布局计算,在文档树状态不稳定时同样会触发内部断言失败。
修复方案
- 移除
OnWrite方法里的Task.Run,所有涉及RichTextBox、FlowDocument的操作全部调度到UI线程执行,禁止任何非UI线程直接访问UI控件的属性、方法。 - 把读取
Blocks.Count、删除旧Block、ScrollToEnd这类UI操作,全部移到UI线程执行分支内,不要在Dispatcher调度外留任何UI访问逻辑。 - 增加窗口状态判断,窗口处于最小化状态时跳过
ScrollToEnd调用,等窗口恢复显示后再执行滚动即可。 - 把
AppendMessage从静态方法改为实例方法,避免静态持有UI控件实例导致的生命周期不一致问题。
修复后核心代码参考
private void OnWrite(string message, Color col) { // 直接调度到UI线程,不要先丢到线程池 if (!Logs.Dispatcher.CheckAccess()) { _ = Logs.Dispatcher.BeginInvoke(new Action(() => AppendMessage(message, col))); return; } AppendMessage(message, col); } // 改为实例方法,不需要传RichTextBox参数 private void AppendMessage(string message, Color col) { lock(MessageLock) { Paragraph paragraph = new Paragraph(new Run(message)); paragraph.Foreground = new System.Windows.Media.SolidColorBrush(new System.Windows.Media.Color { A = col.A, R = col.R, G = col.G, B = col.B }); Logs.Document.Blocks.Add(paragraph); // 旧日志删除逻辑全部在UI线程执行 int removeCount = 0; while (removeCount <= 200 && Logs.Document.Blocks.Count > 300) { Logs.Document.Blocks.Remove(Logs.Document.Blocks.FirstBlock); removeCount++; } // 最小化状态下不执行滚动 var hostWindow = Window.GetWindow(this); if (hostWindow != null && hostWindow.WindowState != WindowState.Minimized) { Logs.ScrollToEnd(); } } } private readonly object MessageLock = new object();
内容的提问来源于stack exchange,提问作者Sye
相关产品推荐
相关产品推荐

