【问题标题】:Task.Run apparently locks UI when nested too deep嵌套太深时,Task.Run 显然会锁定 UI
【发布时间】:2013-05-15 22:37:08
【问题描述】:

我有一个 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>

当我点击“解析”按钮时,我想遍历文件名列表并解析每个文件,报告产品的数量、解析的产品和沿途找到的书籍。显然我希望我的 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;
    }

这一切都执行得非常快,用户界面会快速更新并保持响应速度。但是,如果我将对 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”计数器增加和一切。这是一个可怕的黑客虽然。这意味着什么?

【问题讨论】:

  • 我知道这可能是一个愚蠢的问题:你能用一个更简单的例子重现你的问题吗?例如,使用Threading.Sleep 来模拟工作量?我无法获得与您描述的相同的效果,尽管我正在创建一个非常基本的测试应用程序......
  • 我能看到的唯一区别是,在原始代码中ReportProgress() 是在后台线程上执行的,而在修改后的代码中它是在 UI 线程上执行的。如果在 UI 锁定时暂停调试器,UI 线程的调用堆栈会是什么样子?
  • 这可能是一个消防软管问题。您正在生成如此多的 UI 更新,以至于 UI 线程在试图跟上时完全陷入困境。以适合人眼的速度生成更新。每 50 毫秒一次就足够了。
  • 我不知道,我无法调试该代码。发现通过调用 Sleep() 来减慢工作线程的速度强烈暗示这是一个消防问题。它降低了你打击 UI 线程的速度。不是你想做的,睡一个工作线程是一种浪费。只需少报告进度即可。
  • @BasDL,您正在创建竞争条件:当 UI 线程读取进度信息时,您可能已经更改了它。在您报告相互依赖的多条信息的情况下,您可能会在报告不一致或不正确的情况下发生排序(由于 UI 读取了一些已更改的值,而其他尚未更改的值)改变)。为什么不想你想要报告不可变的值——那样你永远不会遇到麻烦。

标签: c# wpf task-parallel-library async-await


【解决方案1】:

从事件处理程序调用异步方法时,您正在使用等待。这会导致事件处理程序线程等待(不执行任何操作)直到异步方法完成。
来自http://msdn.microsoft.com/en-us/library/vstudio/hh156528.aspx .
await 运算符应用于异步方法中的任务,以暂停该方法的执行,直到等待的任务完成。

【讨论】:

  • 但在这两种情况下,我都在事件处理程序中调用 await。在这两个代码示例中,事件处理程序线程一直等到异步方法完成。那么为什么会有差异呢?
  • 公平点。不同之处在于 Task.Run 实际上是一个同步方法。它将 Action 排队以异步运行并返回。 await 只是在等待 Task.Run 完成,而不是等待 Action 完成。
  • 你确定吗?因为如果我在 await Task.Run() 行上设置断点并跳过它,直到解析完成后才会到达下一行,这表明它正在等待操作。
  • @BrianO''Byrne 这对我来说没有多大意义。 Task.Run() 的重点是异步执行一些代码。 await 将等待 Action 完成。
  • 完全正确 - Task.Run 返回任务,所以很自然的假设是 await 将导致等待,直到返回的任务完成,然后移动到下一行 - 以异步方式。
【解决方案2】:

每次您调用progress.Report(...) 时,您实际上是在向 UI 线程发布一条消息以更新 UI,并且因为您在一个紧密的循环中调用它,您只是在向您的 UI 线程充斥着需要处理的报告消息因此没有时间做任何其他事情(因此锁定)。这就是您的Thread.Sleep(1) 'hack' 起作用的原因,因为您正在给 UI 线程时间赶上。

您需要重新考虑回报告的方式,或者至少重新考虑回帖的频率。您可以使用许多缓冲回发的技术。我会使用Reactive Extensions的解决方案

【讨论】:

    猜你喜欢
    • 2020-07-16
    • 2023-01-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多