在 WPF 中,解释来自 Dispatcher 与来自事件处理程序的 UI 更新顺序 [重复]

Posted

技术标签:

【中文标题】在 WPF 中,解释来自 Dispatcher 与来自事件处理程序的 UI 更新顺序 [重复]【英文标题】:In WPF, explain order of UI updates from Dispatcher vs from event handler [duplicate] 【发布时间】:2021-07-16 19:50:29 【问题描述】:

我想了解以下行为。 我有带有按钮单击事件处理程序的 WPF 应用程序,我在其中启动 Parallel.ForEach。在每个循环中,我通过 Dispatcher 更新 UI。在Parallel.Foreach 之后,我对 UI 进行了“最终更新”。然而,这个“最终更新”实际上发生在来自Dispatcher 的任何更新之前。为什么会按这个顺序发生?

private void btnParallel_Click(object sender, RoutedEventArgs e)

    txbResultsInfo.Text = "";

    Parallel.ForEach(_files, (file) =>
    
        string partial_result = SomeComputation(file);

        Dispatcher.BeginInvoke(new Action(() =>
        
            txbResultsInfo.Text += partial_result + Environment.NewLine;
        ));
    );

    txbResultsInfo.Text += "-- COMPUTATION DONE --"; // THIS WILL BE FIRST IN UI, WHY?
    //Dispatcher.BeginInvoke(new Action(() => txbResultsInfo.Text += "-- COMPUTATION DONE --"; - THIS WAY IT WILL BY LAST IN UI

我的直觉期望是代码在 Parallel.ForEach 循环的所有分支完成后继续,这意味着 Dispatcher 已收到所有 UI 更新请求并开始执行它们,然后我们才继续更新 UI 从处理程序方法的其余部分。 但"-- COMPUTATION DONE --" 实际上总是首先出现在 textBlock 中。即使我将Task.Delay(5000).Wait() 放在“完成计算”更新之前。因此,这不仅仅是速度问题,实际上以某种方式排序,此更新发生在 Dispatcher 更新之前。

如果我将“完成计算”更新也放入调度程序,它的行为与我预期的一样,并且位于文本的末尾。但是为什么这也需要通过 dispatcher 来完成呢?

【问题讨论】:

这确实很奇怪。通常Parallel.ForEach 方法应该等到一切都完成然后再进一步......也许如果你在foreach 的末尾添加.IsCompleted? (虽然我认为这不会解决它) 我会指出,由于这是 WPF 代码,因此您的代码直接设置 UI 属性无论如何都是错误的。您应该使用数据绑定,并让 WPF 框架处理跨线程调度。这不一定会改变您在上面看到的内容,但至少会是更好的代码。 BeginInvoke() 不会等待。它只是排队你派出的委托并返回。在您的示例中,所有已调度的委托都由 Parallel.ForEach() 排队,这会阻塞 UI 线程,否则会调用排队的委托,因此该方法返回并让对 txbResultsInfo.Text 的分配发生在任何排队的委托到达之前实际上被称为。有关详细信息,请参阅副本(所有 BeginXXX()XXX() 方法都以这种方式工作)。 【参考方案1】:

Parallel.ForEach是一种阻塞方法,意思是UI线程在并行执行期间被阻塞。因此,发布到Dispatcher 的操作无法执行,而是在队列中缓冲。并行执行完成后,代码继续运行,直到事件处理程序结束,然后才执行排队的操作。这种行为不仅会打乱进度消息的顺序,还会导致 UI 无响应,这可能同样令人讨厌。

要解决这两个问题,您应该避免在 UI 线程上运行并行循环,而是在后台线程上运行它。更简单的方法是让您的处理程序async,并将循环包装在await Task.Run 中,如下所示:

private async void btnParallel_Click(object sender, RoutedEventArgs e)

    txbResultsInfo.Text = "";

    await Task.Run(() =>
    
        Parallel.ForEach(_files, (file) =>
        
            string partial_result = SomeComputation(file);

            Dispatcher.BeginInvoke(new Action(() =>
            
                txbResultsInfo.Text += partial_result + Environment.NewLine;
            ));
        );
    );

    txbResultsInfo.Text += "-- COMPUTATION DONE --";

但老实说,使用Dispatcher 报告进度是一种古老而尴尬的方法。现代方法是使用IProgress<T> 抽象。以下是你可以如何使用它:

private async void btnParallel_Click(object sender, RoutedEventArgs e)

    txbResultsInfo.Text = "";
    IProgress<string> progress = new Progress<string>(message =>
    
        txbResultsInfo.Text += message;
    );

    await Task.Run(() =>
    
        Parallel.ForEach(_files, (file) =>
        
            string partial_result = SomeComputation(file);
            progress.Report(partial_result + Environment.NewLine);
        );
    );
    progress.Report("-- COMPUTATION DONE --");

如果上面的代码不是一目了然,可以在这里找到扩展教程:Enabling Progress and Cancellation in Async APIs

旁注:Parallel.For/Parallel.ForEach 方法的默认行为是saturate the ThreadPool,这可能会带来很大的问题,尤其是对于启用异步的应用程序。出于这个原因,我建议在每次使用这些方法时明确指定 MaxDegreeOfParallelism 选项:

Parallel.ForEach(_files, new ParallelOptions()

    MaxDegreeOfParallelism = Environment.ProcessorCount
, (file) =>

    //...
);

【讨论】:

以上是关于在 WPF 中,解释来自 Dispatcher 与来自事件处理程序的 UI 更新顺序 [重复]的主要内容,如果未能解决你的问题,请参考以下文章

2022-04-20 WPF面试题 WPF中Dispatcher对象的用途是什么?

请帮忙解释下Dispatcher.BeginInoke((Action)(()=>....;...;...;),null);啥意思?

WPF入门教程系列四——Dispatcher介绍

Dispatcher.BeginInvoke(new...ProgressBarProcess.Value = 1 没有显示?

WPF Dispatcher.BeginInvoke子线程更新UI

WPF 线程 Dispatcher