Task.Run seems to block the user interface when it is too deeply nested

I have a WPF application that needs to parse a bunch of large XML files (about 40 MB) containing products and store information about all the products that are actually books. For the progress report, I have a datagrid that displays the file name, status ("pending", "parsing", "completed"), the number of products found, the number of products analyzed and the number of books found, for example this:

<DataGrid Grid.ColumnSpan="2" Grid.Row="1" ItemsSource="{Binding OnixFiles}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False" CanUserSortColumns="False"> <DataGrid.Columns> <DataGridTextColumn Header="Bestand" IsReadOnly="True" Binding="{Binding FileName}" SortMemberPath="FileName" /> <DataGridTextColumn Header="Status" IsReadOnly="True" Binding="{Binding Status}" /> <DataGridTextColumn Header="Aantal producten" IsReadOnly="True" Binding="{Binding NumTotalProducts}" /> <DataGridTextColumn Header="Verwerkte producten" IsReadOnly="True" Binding="{Binding NumParsedProducts}" /> <DataGridTextColumn Header="Aantal geschikte boeken" IsReadOnly="True" Binding="{Binding NumSuitableBooks}" /> </DataGrid.Columns> </DataGrid> 

When I click the "Parse" button, I want to iterate over the list of file names and analyze each file, reporting the number of products, the products being analyzed, and books on this path. Obviously, I want my user interface to remain responsive, so I want to parse in another thread using Task.Run ().

When the user clicks the button labeled "Parse", the application should begin parsing the files. If I call TaskRun directly in the command_executed command, everything works fine:

  private async void ParseFilesCommand_Executed(object sender, ExecutedRoutedEventArgs e) { foreach (var f in OnixFiles) { await Task.Run(() => f.Parse()); } } // In the OnixFileViewModel public void Parse() { var progressIndicator = new Progress<ParsingProgress>(ReportProgress); var books = Parser.ParseFile(this.fileName, progressIndicator); } private void ReportProgress(ParsingProgress progress) { // These are properties that notify the ui of changes NumTotalProducts = progress.NumTotalProducs; NumParsedProducts = progress.NumParsedProducts; NumSuitableBooks = progress.NumSuitableBooks; } // In the class Parser public static IEnumerable<Book> ParseFile(string filePath, IProgress<ParsingProgress> progress) { List<Book> books = new List<Book>(); var root = XElement.Load(filePath); var fileInfo = new FileInfo(filePath); XNamespace defaultNamespace = "http://www.editeur.org/onix/3.0/reference"; var products = (from p in XElement.Load(filePath).Elements(defaultNamespace + "Product") select p).ToList(); var parsingProgress = new ParsingProgress() { NumParsedProducts = 0, NumSuitableBooks = 0, NumTotalProducs = products.Count }; progress.Report(parsingProgress); foreach (var product in products) { // Complex XML parsing goes here parsingProgress.NumParsedProducts++; if (...) // If parsed product is actual book { parsingProgress.NumSuitableBooks++; } progress.Report(parsingProgress); } return books; } 

Everything is done superfast, ui is updated quickly and remains responsive. However, if I transfer the call to Task.Run () to the ParseFile method, like this:

  private async void ParseFilesCommand_Executed(object sender, ExecutedRoutedEventArgs e) { foreach (var f in OnixFiles) { await f.ParseAsync(); } } // In the OnixFileViewModel public async Task ParseAsync() { var progressIndicator = new Progress<ParsingProgress>(ReportProgress); var books = await Parser.ParseFileAsync(this.fileName, progressIndicator); } private void ReportProgress(ParsingProgress progress) { // These are properties that notify the ui of changes NumTotalProducts = progress.NumTotalProducs; NumParsedProducts = progress.NumParsedProducts; NumSuitableBooks = progress.NumSuitableBooks; } // In the class Parser public static async Task<IEnumerable<Book>> ParseFileAsync(string filePath, IProgress<ParsingProgress> progress) { List<Book> books = new List<Book>(); await Task.Run(() => { var root = XElement.Load(filePath); var fileInfo = new FileInfo(filePath); XNamespace defaultNamespace = "http://www.editeur.org/onix/3.0/reference"; var products = (from p in XElement.Load(filePath).Elements(defaultNamespace + "Product") select p).ToList(); var parsingProgress = new ParsingProgress() { NumParsedProducts = 0, NumSuitableBooks = 0, NumTotalProducs = products.Count }; progress.Report(parsingProgress); foreach (var product in products) { // Complex XML parsing goes here parsingProgress.NumParsedProducts++; if (...) // If parsed product is actual book { parsingProgress.NumSuitableBooks++; } progress.Report(parsingProgress); } }); return books; } 

The user interface is blocked, not updated until the file has completed parsing and everything will look much slower.

What am I missing? Why does it work as expected if you call Task.Run () in the command_executed handler, but not if you call it in the async method called by this method?

Edit: at the request of Shaamaan, here is a simpler example of what I'm doing (using just thread.sleep to simulate a workload), but unfortunately the sample works the way I originally expected it, failing to isolate the problem I'm experiencing . However, adding it for completeness:

MainWindow.xaml:

 <Window x:Class="ThreadingSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <StackPanel> <DataGrid Grid.ColumnSpan="2" Grid.Row="1" ItemsSource="{Binding Things}" AutoGenerateColumns="False" Height="250" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False" CanUserSortColumns="False"> <DataGrid.Columns> <DataGridTextColumn Header="Name" IsReadOnly="True" Binding="{Binding Name}" /> <DataGridTextColumn Header="Value" IsReadOnly="True" Binding="{Binding Value}" /> </DataGrid.Columns> </DataGrid> <Button Click="RightButton_Click">Right</Button> <Button Click="WrongButton_Click">Wrong</Button> </StackPanel> </Window> 

MainWindow.xaml.cs:

 using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace ThreadingSample { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public ObservableCollection<Thing> Things { get; private set; } public MainWindow() { InitializeComponent(); this.DataContext = this; Things = new ObservableCollection<Thing>(); for (int i = 0; i < 200; i++) { Things.Add(new Thing(i)); } } private async void RightButton_Click(object sender, RoutedEventArgs e) { foreach (var t in Things) { await Task.Run(() => t.Parse()); } } private async void WrongButton_Click(object sender, RoutedEventArgs e) { foreach (var t in Things) { await t.ParseAsync(); } } } } 

Thing.cs:

 using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ThreadingSample { public class Thing : INotifyPropertyChanged { private string _name; public string Name { get { return _name; } set { _name = value; RaisePropertyChanged("Name"); } } private int _value; public int Value { get { return _value; } set { _value = value; RaisePropertyChanged("Value"); } } public Thing(int number) { Name = "Thing nr. " + number; Value = 0; } public void Parse() { var progressReporter = new Progress<int>(ReportProgress); HeavyParseMethod(progressReporter); } public async Task ParseAsync() { var progressReporter = new Progress<int>(ReportProgress); await HeavyParseMethodAsync(progressReporter); } private void HeavyParseMethod(IProgress<int> progressReporter) { for (int i = 0; i < 1000; i++) { Thread.Sleep(10); progressReporter.Report(i); } } private async Task HeavyParseMethodAsync(IProgress<int> progressReporter) { await Task.Run(() => { for (int i = 0; i < 1000; i++) { Thread.Sleep(100); progressReporter.Report(i); } }); } private void ReportProgress(int progressValue) { this.Value = progressValue; } private void RaisePropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } public event PropertyChangedEventHandler PropertyChanged; } } 

The only difference between this sample and my real code that I can tell is that my real code parses a bunch of 40 MB files using LINQ to XML, while this example just calls Thread.Sleep ().

Edit 2: I found a terrifying workaround. If I use the second method and call Thread.Sleep (1) after each product is parsed and before calling IProgress.Report () everything works fine. I see that the "NumParsedProducts" counter is incrementing and that’s it. This is a terrible hack. What does this mean?

+4
source share
2 answers

Each time you call progress.Report(...) , you efficiently send a message to the user interface thread to update the user interface, and since you call it in a narrow loop, you simply flood the user interface with message reports that need to be processed and thus do not get the time to do anything else (and thereby block). This is why your Thread.Sleep(1) 'hack' works because you give the user interface thread time to catch up.

You need to rethink how you communicate, or at least how often you send back. You can use many methods for buffering mail messages. I would use the Reactive Extensions solution

+2
source

You use wait when calling the async method from an event handler. This causes the event handler thread to wait (do nothing) until the asynchronous method is executed.
From http://msdn.microsoft.com/en-us/library/vstudio/hh156528.aspx .
The wait statement is applied to a task in an asynchronous method to pause the method until the expected task completes.

-2
source

Source: https://habr.com/ru/post/1481894/


All Articles