在 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);啥意思?
Dispatcher.BeginInvoke(new...ProgressBarProcess.Value = 1 没有显示?