I would like to understand the following behaviour.
I have WPF application with button click event handler in which I start Parallel.ForEach
. In each loop I update UI via Dispatcher. After the Parallel.Foreach
I do the "final update" of the UI. However this "final update" actually happens before any of the updates from the Dispatcher
. Why is it happening in this order?
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
}
My intuitive expectation was that the code continues after all branches of the Parallel.ForEach
loops are finished, meaning the Dispatcher
has received all requests for UI update and started to execute them and only then we continue to update the UI from the rest of the handler method.
But the "-- COMPUTATION DONE --"
actually always appears first in the textBlock. Even if I put Task.Delay(5000).Wait()
before the "done computation" update. So it is not just a matter of speed, it is actually sorted somehow that this update is happening before updates from Dispatcher.
If I put the "done computation" update to dispatcher as well, it behaves as I would expect and is at the end for the text. But why does this needs to be done via dispatcher as well?
The Parallel.ForEach
is a blocking method, meaning that the UI thread is blocked during the parallel execution. So the actions posted to the Dispatcher
cannot be executed, and instead they are buffered in a queue. After the completion of the parallel execution the code continues running until the end of the event handler, and only then the queued actions are executed. This behavior not only messes up the order of the progress messages, but also makes the UI not-responsive, which is probably equally annoying.
To fix both problems you should avoid running the parallel loop on the UI thread, and instead run it on a background thread. The easier way to do it is to make your handler async
, and wrap the loop in an await Task.Run
like this:
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 --";
}
But honestly using the Dispatcher
for reporting progress is an old and awkward approach. The modern approach is to use the IProgress<T>
abstraction. Here is how you could use it:
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 --");
}
In case the above code is not self-explanatory, an extended tutorial can be found here: Enabling Progress and Cancellation in Async APIs
Side note: The default behavior of the Parallel.For
/Parallel.ForEach
methods is to saturate the ThreadPool
, which can be quite problematic, especially for async-enabled applications. For this reason I recommend specifying explicitly the MaxDegreeOfParallelism
option, every time these methods are used:
Parallel.ForEach(_files, new ParallelOptions()
{
MaxDegreeOfParallelism = Environment.ProcessorCount
}, (file) =>
{
//...
});