为什么 IProgress<T> 的 Report(T) 方法会阻塞 UI 线程?

4
我在下面的代码中使用了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行左右的写操作中更新进度,因为这不是线程安全的。 - boateng
1
显然,UI的消息泵线程在某种程度上被阻塞了。在这方面,WPF的行为与Winforms并没有太大的区别。您考虑过使用BackgroundWorker吗? - Robert Harvey
此外,“totalLines = System.IO.File.ReadLines(@"D:\test.txt").Count();”不是必须读取整个文件才能得到结果吗?也许更好的方法是从文件大小和前100行的抽样估计行数,并求出平均行长。 - Robert Harvey
@RobertHarvey 计算行数对问题没有影响,那一行非常快,只有大约50,000行。 - erotavlas
@numbtongue 生成文件肯定是完成的,而且在开始读取之前streamwriter已经关闭。我不认为这是个问题。 - erotavlas
1个回答

7
我猜测您的问题是您报告进度的频率过高。如果在Report调用之间所做的工作很琐碎(例如仅从文件中读取一行),则将操作分派到UI线程将成为瓶颈。您的UI调度程序队列被淹没了,并且无法跟上新事件,如响应鼠标单击或移动。
为了减轻这种情况,您应该将Report调用的频率降低到合理水平--例如,只在处理1,000行批次时调用它。
int i = 0;
foreach (string line in System.IO.File.ReadLines(file, Encoding.UTF8))
{
    if (++i % 1000 == 0)
        progress.Report(1000);
}

回应评论:选择批大小时文件大小并不重要。相反:找到一个合理的更新频率目标——比如100毫秒。测量或估计读取和处理一行数据的时间——例如100微秒。将前者除以后者,就能得出答案。我们选择了1000,因为我们估计处理1000行数据需要100毫秒。最佳的更新频率大约在10-100毫秒左右,这是人类感知的极限;任何更频繁的更新用户都无法注意到。

根据上述内容,你处理10行和500行的文件不需要向用户界面发出任何更新,因为它们会在用户有机会观察到进度之前在几毫秒内完全处理完毕。100万行的文件总共需要大约100秒钟,并且在此期间将更新UI 1000次(每100毫秒一次)。


好的,但是我们怎么知道更新会锁定UI的频率?我的意思是要报告多少行的百分比? - erotavlas
1
是的,我确实尝试过这个方法,性能得到了显著提升,并且进度条也正常工作了。 - Robert Harvey
1
@erotavlas:测试和实验。我尝试了1000行,看起来很合理。 - Robert Harvey
1
文件大小并不重要。为更新频率找到一个合理的目标,比如100毫秒。测量或估计读取和处理一行所需的时间,例如100纳秒。将前者除以后者,就可以得出答案。 - Douglas
2
另一种建立该频率的方法是:只需在一个公共变量中保持计数(使用 Interlocked.Increment()),并让 DispatcherTimer 报告进度。 - H H
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接