4

製品を含む大量の大きな XML ファイル (約 40MB) を解析し、実際に本であるすべての製品に関する情報を保存する必要がある WPF アプリケーションがあります。進捗レポートのために、ファイル名、ステータス (「待機中」、「解析中」、「完了」など)、見つかった製品の数、解析された製品の数、見つかった本の数を表示するデータグリッドがあります。これ:

        <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>

「解析」ボタンを押すと、ファイル名のリストを反復処理して各ファイルを解析し、途中で製品、解析した製品、見つけた本の量を報告します。明らかに、UI の応答性を維持したいので、Task.Run() を使用して別のスレッドで解析を実行したいと考えています。

ユーザーが「解析」というラベルの付いたボタンを押すと、アプリケーションはファイルの解析を開始する必要があります。ボタン コマンドの 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 を使用して 40 MB の xml ファイルの束を解析しているのに対し、このサンプルでは単に Thread.Sleep() を呼び出していることです。

編集 2:恐ろしい回避策を見つけました。2 番目の方法を使用して、各製品が解析された後、IProgress.Report() を呼び出す前に Thread.Sleep(1) を呼び出すと、すべて正常に動作します。「NumParsedProducts」カウンターの増加とすべてを確認できます。しかし、これはひどいハックです。それはどういう意味ですか?

4

2 に答える 2

2

呼び出すたびprogress.Report(...)に、UI スレッドに効果的にメッセージを投稿して UI を更新しています。これをタイトなループで呼び出しているため、処理が必要なレポート バック メッセージで UI スレッドをあふれさせるだけであり、処理する時間がありません。他のことをします(したがって、ロックします)。Thread.Sleep(1)UIスレッドに追いつく時間を与えているため、「ハック」が機能しているのはそのためです。

報告する方法、または少なくとも投稿する頻度を再考する必要があります。ポストバックをバッファリングする多くの手法を使用できます。Reactive Extensionsのソリューションを使用していたでしょう

于 2013-05-22T15:13:23.177 に答える