You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();

根因定位

  1. 跨线程UI访问逻辑漏洞:当前实现用Task.Run把日志追加逻辑丢到线程池执行,虽然加了Dispatcher调度判断,但删除旧日志块的逻辑(读取Blocks.Count、删除FirstBlock)写在了Dispatcher判断分支外。非UI线程进入方法时,通过Invoke切回UI线程完成段落添加后,会回到线程池线程继续执行后面的文档访问操作,直接跨线程操作RichTextBox的文档树。WPF的FlowDocument内部用伸展树(SplayTree)实现,遇到跨线程并发访问时会触发内部一致性断言,直接调用FailFast终止进程,这类错误无法被常规try/catch捕获,和堆栈报错位置完全吻合。
  2. 最小化状态放大并发冲突概率:窗口最小化时WPF会临时挂起部分可视化树渲染、布局计算逻辑,文档树状态切换的窗口期如果被后台线程访问,比窗口正常显示时更容易触发内部状态校验失败,这也是最小化时必现、正常状态下偶现的原因。
  3. 锁机制无法规避冲突:当前的静态MessageLock只能锁自定义代码的执行路径,无法限制WPF UI线程本身对文档树的访问,完全挡不住并发访问问题。
  4. 最小化状态调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 21:57:23