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

In WPF, explain order of UI updates from Dispatcher vs from event handler

我想了解以下行为。 我有一个带有按钮单击事件处理程序的 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 更新之前。

如果我也将“完成计算”更新放入调度程序,它的行为将如我所料,并且在文本的末尾。但为什么这也需要通过调度程序来完成?

Parallel.ForEach是阻塞方式,意思是UI线程在并行执行的时候是阻塞的。因此,发布到 Dispatcher 的操作无法执行,而是缓冲在队列中。并行执行完成后,代码继续 运行ning 直到事件处理程序结束,然后才执行排队的操作。这种行为不仅打乱了进度消息的顺序,而且使 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, which can be quite problematic, especially for async-enabled applications. For this reason I recommend specifying explicitly the MaxDegreeOfParallelism 选项,每次使用这些方法时:

Parallel.ForEach(_files, new ParallelOptions()
{
    MaxDegreeOfParallelism = Environment.ProcessorCount
}, (file) =>
{
    //...
});