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

问题描述

我想了解以下行为。 我有一个带有按钮单击事件处理程序的 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 的操作,而是将它们缓存在队列中。并行执行完成后,代码继续运行,直到事件处理程序结束,然后才执行排队的动作。这种行为不仅打乱了进度消息的顺序,而且使 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) =>
{
    //...
});