如何解决在 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) =>
{
//...
});
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。