【问题标题】:Why is IProgress<T> Report(T) method blocking the UI thread?为什么 IProgress<T> Report(T) 方法会阻塞 UI 线程?
【发布时间】:2018-07-05 20:23:42
【问题描述】:

我在下面的代码中的 Process2() 方法中报告进度时遇到问题。我想在读取每一行后增加进度条,但这样做会阻塞 UI 并且它变得无响应。如果我注释掉 progress.Report() 行,它不再阻塞 UI 线程。有谁知道为什么会发生这种情况以及我该如何解决?

这是可以粘贴到入门 WPF 应用程序中的完整工作代码。

单击“运行”按钮(开始生成文件时可能会稍作停顿,等到“生成文件完成”之后)并尝试移动窗口,它仍然保持冻结状态。

警告:此代码将在您的 bin\Debug 文件夹(或您的配置指向的任何内容)中生成一个文本文件如果您从网络路径运行它可能无法写入此文件,因此建议从本地磁盘。

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
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;
using System.Windows.Threading;

namespace WpfApplication2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        Queue<string> queue = new Queue<string>();

        List<string> stringsCollection = new List<string>() { "1_abc123_A_AA_zzz", "2_abc123_AAAA_zzz", "3_abc123_AAAAAA_zzz" };

        int linesCount = 0;
        int totalLines = 0;

        string ASSEMBLY_PATH;
        string file; 
        public MainWindow()
        {
            InitializeComponent();

            ASSEMBLY_PATH = ReturnThisAssemblyPath();
            file = ASSEMBLY_PATH + @"\test.txt";
            generateFile();
        }

        private async void Button_Click2(object sender, RoutedEventArgs e)
        {
            linesCount = 0;

            Progress<int> process2_progress;

            this.progress.Value = 0;
            this.status.Text = "";

            process2_progress = new Progress<int>();
            process2_progress.ProgressChanged += Process2_progress_ProgressChanged;

            this.status.Text += "Searching..." + Environment.NewLine;
            await Task.Run(() =>
            {
                totalLines = System.IO.File.ReadLines(file).Count();

                foreach (string s in stringsCollection)
                {
                    Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)(() =>
                    {
                        this.status.Text += "Searching " + s + Environment.NewLine;
                    }));
                    List<string> strCollection = Process2(s, process2_progress);

                    foreach (string str in strCollection)
                        queue.Enqueue(str);
                }
            });

            this.status.Text += "DONE!!" + Environment.NewLine;
        }

        private void Process2_progress_ProgressChanged(object sender, int e)
        {
            linesCount += e;
            this.progress.Value = linesCount * 100 / totalLines;
        }

        List<string> Process2(string inputString, IProgress<int> progress)
        {
            List<string> result = new List<string>();

            foreach (string line in System.IO.File.ReadLines(file, new UTF8Encoding()))
            {
                progress.Report(1);
            }

            return result;
        }

    void generateFile()
    {
        this.status.Text += "Generating FIle..." + Environment.NewLine;
        int count = 0;
        using (StreamWriter sw = new StreamWriter(file, true))
        {
            do
            {
                sw.WriteLine(Guid.NewGuid().ToString());
                count++;
            } while (count < 51000);

        }
        this.status.Text += "Done Generating FIle!" + Environment.NewLine;
    }

        public string ReturnThisAssemblyPath()
        {
            string codeBase = Assembly.GetAssembly(typeof(MainWindow)).CodeBase;
            UriBuilder uri = new UriBuilder(codeBase);
            string path = Uri.UnescapeDataString(uri.Path);
            return System.IO.Path.GetDirectoryName(path);
        }
    }
}

MainWindow.xaml

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication2"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TextBox x:Name="status" Grid.Row="0"></TextBox>

        <Button Grid.Row="2" Height="50" Click="Button_Click2">Run2</Button>
        <ProgressBar x:Name="progress" Grid.Row="3" Height="20" ></ProgressBar>
    </Grid>
</Window>

【问题讨论】:

  • 可能是您的文本文件在写入操作期间被锁定,并且在读取进度条期间无法可靠访问。尝试在每 100 行左右之后从写入操作中更新进度,因为这不是线程安全..
  • 好吧,显然 UI 的消息泵线程在某种程度上被阻塞了。在这方面,WPF 的行为与 Winforms 并没有什么不同。您是否考虑过使用 BackgroundWorker?
  • 另外,totalLines = System.IO.File.ReadLines(@"D:\test.txt").Count(); 不是必须读取整个文件才能得到结果吗?也许最好从文件中的字节数和前 100 行的采样中估计行数以获得平均行长。
  • @RobertHarvey 计算行数对问题没有影响,该行很快就完成了,它只有大约 50,000 行
  • @numbtongue 生成文件肯定已完成,并且流写入器在继续读取之前已关闭。我认为这不是问题。

标签: c# .net wpf asynchronous


【解决方案1】:

我怀疑您的问题是您过于频繁地报告进度。如果您在Report 调用之间所做的工作是微不足道的(例如从文件中仅读取一行),那么将操作分派到 UI 线程将成为您的瓶颈。您的 UI 调度程序队列被淹没,并且无法跟上新事件,例如响应鼠标单击或移动。

为了缓解这种情况,您应该将Report 调用的频率降低到合理的水平——例如,仅在处理一批 1000 行时调用它。

int i = 0;
foreach (string line in System.IO.File.ReadLines(file, Encoding.UTF8))
{
    if (++i % 1000 == 0)
        progress.Report(1000);
}

回应 cmets:选择批量大小时文件大小无关紧要。而是:为更新频率找到一个合理的目标——比如 100 毫秒。测量或估计读取和处理一行所需的时间——例如,100μs。将前者除以后者,你就会得到答案。我们选择 1,000 行是因为我们估计 1,000 行需要 100 毫秒来处理。最佳更新频率在 10-100ms 左右,这是人类感知的极限;用户不会注意到比这更频繁的任何事情。

根据上述内容,您的 10 行和 500 行文件不需要对 UI 进行任何更新,因为在用户有机会观察任何进展之前,它们会在几毫秒内完成处理。 1,000,000 行的文件总共需要大约 100 秒,在此期间它会更新 UI 1,000 次(每 100 毫秒一次)。

【讨论】:

  • 好的,但是我们如何知道更新会锁定 UI 的频率?我的意思是要报告的行的百分比是多少?
  • 是的,我就是这么试的,性能提升明显,进度条确实有效。
  • @erotavlas:测试和实验。我用 1000 行试了一下,看起来是合法的。
  • 文件大小无关紧要。为更新频率找到一个合理的目标——比如 100 毫秒。测量或估计读取和处理一行所需的时间——例如,100ns。将前者除以后者,即可得到答案。
  • 另一种确定频率的方法:只需在一个公共变量中保存一个计数(使用Interlocked.Increment())并让 DispatcherTimer 报告进度。
猜你喜欢
  • 2019-03-21
  • 2021-08-29
  • 1970-01-01
  • 1970-01-01
  • 2016-06-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多