为什么IProgress Report(T)方法阻塞了UI线程?

时间:2021-12-12 20:56:45

I have a problem with reporting progress within the method Process2() in my code below. I want to increment the progress bar after every line that is read, but doing so blocks the UI and it becomes unresponsive. If I comment out the progress.Report() line, it no longer blocks UI thread. Does anyone know why this happens and how I can fix it?

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

Here is fully working code that can be pasted into a starter WPF application.

这是完全可用的代码,可以粘贴到启动WPF应用程序中。

Click the Run button (there might be a small pause at the beginning for generating the file, wait until after Generating file is Done) and try to move the window around, it remains frozen.

单击“运行”按钮(开头可能会有一个小停顿来生成文件,等到生成文件完成后)并尝试移动窗口,它仍然处于冻结状态。

WARNING: this code will generate a text file in your bin\Debug folder (or whatever your configuration is pointing to) It may not be able to write this file if you run this from a network path, thus recommended to run from local disk.

警告:此代码将在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>

1 个解决方案

#1


6  

I suspect your issue is that you're reporting progress too frequently. If the work that you're doing between Report calls is trivial (such as reading just one line from a file), then the dispatching of operations to the UI thread will become your bottleneck. Your UI dispatcher queue becomes flooded, and it won't keep up with new events such as responding to mouse clicks or moves.

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

To mitigate this, you should reduce the frequency of your Report calls to a sensible level -- for example, only calling it when a batch of 1,000 lines are processed.

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

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

In response to the comments: The file size doesn't matter when picking the batch size. Rather: Find a sensible target for the update frequency -- say, 100 ms. Measure or estimate the time it takes to read and process one line -- for example, 100 μs. Divide the former by the latter, and you get your answer. We picked 1,000 because we estimate that 1,000 lines would take 100 ms to process. The optimal update frequency lies around 10–100 ms, which is the limit of human perception; anything more frequent than that will not be noticed by the user.

回复评论:选择批量大小时,文件大小无关紧要。相反:为更新频率找到合理的目标 - 比方说,100ms。测量或估计读取和处理一条线所需的时间 - 例如,100μs。将前者除以后者,你得到答案。我们选择1,000,因为我们估计1000行需要100毫秒才能处理。最佳更新频率约为10-100ms,这是人类感知的极限;任何比这更频繁的事情都不会被用户注意到。

Per the above, your 10- and 500-line files don't need to issue any updates to the UI, because they would have been processed completely in mere milliseconds, before the user has the chance to observe any progress. The 1,000,000-line file would take around 100 seconds in total, and it will update the UI 1,000 times during that period (once every 100 ms).

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

#1


6  

I suspect your issue is that you're reporting progress too frequently. If the work that you're doing between Report calls is trivial (such as reading just one line from a file), then the dispatching of operations to the UI thread will become your bottleneck. Your UI dispatcher queue becomes flooded, and it won't keep up with new events such as responding to mouse clicks or moves.

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

To mitigate this, you should reduce the frequency of your Report calls to a sensible level -- for example, only calling it when a batch of 1,000 lines are processed.

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

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

In response to the comments: The file size doesn't matter when picking the batch size. Rather: Find a sensible target for the update frequency -- say, 100 ms. Measure or estimate the time it takes to read and process one line -- for example, 100 μs. Divide the former by the latter, and you get your answer. We picked 1,000 because we estimate that 1,000 lines would take 100 ms to process. The optimal update frequency lies around 10–100 ms, which is the limit of human perception; anything more frequent than that will not be noticed by the user.

回复评论:选择批量大小时,文件大小无关紧要。相反:为更新频率找到合理的目标 - 比方说,100ms。测量或估计读取和处理一条线所需的时间 - 例如,100μs。将前者除以后者,你得到答案。我们选择1,000,因为我们估计1000行需要100毫秒才能处理。最佳更新频率约为10-100ms,这是人类感知的极限;任何比这更频繁的事情都不会被用户注意到。

Per the above, your 10- and 500-line files don't need to issue any updates to the UI, because they would have been processed completely in mere milliseconds, before the user has the chance to observe any progress. The 1,000,000-line file would take around 100 seconds in total, and it will update the UI 1,000 times during that period (once every 100 ms).

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