我有一个WPF应用程序需要解析一堆包含产品的大型XML文件(大约40MB),并保存有关所有实际书籍产品的信息。对于进度报告,我有一个数据网格,显示文件名,状态(“等待”,“解析”,“完成”,那种东西),找到的产品数量,解析的产品数量和找到的书籍数量,如这样:
<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>
当我点击“Parse”按钮时,我想遍历文件名列表并解析每个文件,报告产品数量,解析产品和找到的书籍。显然我希望我的UI保持响应,所以我想使用Task.Run()在不同的线程上进行解析。
当用户点击标有“Parse”的按钮时,应用程序需要开始解析文件。如果我在按钮命令的command_executed方法中调用TaskRun,一切正常:
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;
}
它全部执行速度超快,ui快速更新并保持响应。但是,如果我将调用Task.Run()移动到ParseFile方法中,如下所示:
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;
}
UI锁定,直到文件完成解析后才更新,所有内容都显得慢得多。
我错过了什么?如果在command_executed处理程序中调用Task.Run(),为什么它按预期工作,但如果在该方法调用的异步方法中调用它,则不会如此?
按照Shaamaan的要求编辑,这是我正在做的一个更简单的示例(使用简单的thread.sleep来模拟工作负载),但令人沮丧的是,示例的工作方式与我原先预期的一样,没有突出我遇到的问题。仍然,添加它是为了完整性:
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;
}
}
我可以告诉这个示例和我的现实代码之间的唯一区别是,我的现实代码使用LINQ to XML解析了一堆40mb xml文件,而这个示例只调用了Thread.Sleep()。
编辑2:我发现了一个可怕的解决方法。如果我使用第二种方法并在解析每个产品之后调用Thread.Sleep(1)并且在调用IProgress.Report()之前,一切正常。我可以看到“NumParsedProducts”计数器增加了一切。这是一个可怕的黑客。这意味着什么?
答案 0 :(得分:2)
每次调用progress.Report(...)
时,您都有效地向UI线程发送消息以更新UI,并且因为您在紧密循环中调用此消息,所以您只需将UI线程充满其需要的报告消息处理,因此没有时间做任何其他事情(从而锁定)。这就是为什么你的Thread.Sleep(1)
'hack'正在工作的原因,因为你给了UI线程时间来赶上。
您需要重新考虑报告的方式或至少重新发布的频率。你可以使用许多缓冲后置的技术。我会使用Reactive Extensions
的解决方案答案 1 :(得分:-2)
从事件处理程序调用async方法时,您正在使用await。这会导致事件处理程序线程等待(不执行任何操作),直到异步方法完成为止
来自http://msdn.microsoft.com/en-us/library/vstudio/hh156528.aspx。
await运算符应用于异步方法中的任务,以暂停方法的执行,直到等待的任务完成。