为什么 File.ReadAllLinesAsync() 会阻塞 UI 线程?

问题描述 投票:0回答:2

这是我的代码。读取文件行的 WPF 按钮的事件处理程序:

private async void Button_OnClick(object sender, RoutedEventArgs e)
{
    Button.Content = "Loading...";
    var lines = await File.ReadAllLinesAsync(@"D:\temp.txt"); //Why blocking UI Thread???
    Button.Content = "Show"; //Reset Button text
}

我在 .NET Core 3.1 WPF 应用程序中使用了异步版本的

File.ReadAllLines()
方法。

但是它阻塞了 UI 线程!为什么?


更新:与@Theodor Zoulias相同,我做了一个测试:

private async void Button_OnClick(object sender, RoutedEventArgs e)
{
    Button.Content = "Loading...";
    TextBox.Text = "";

    var stopwatch = Stopwatch.StartNew();
    var task = File.ReadAllLinesAsync(@"D:\temp.txt"); //Problem
    var duration1 = stopwatch.ElapsedMilliseconds;
    var isCompleted = task.IsCompleted;
    stopwatch.Restart();
    var lines = await task;
    var duration2 = stopwatch.ElapsedMilliseconds;

    Debug.WriteLine($"Create: {duration1:#,0} msec, Task.IsCompleted: {isCompleted}");
    Debug.WriteLine($"Await:  {duration2:#,0} msec, Lines: {lines.Length:#,0}");

    Button.Content = "Show";
}

结果是:

Create: 652 msec msec, Task.IsCompleted: False | Await:   15 msec, Lines: 480,001

.NET Core 3.1、C# 8、WPF、调试构建 | 7.32 Mb 文件(.txt) |硬盘 5400 SATA

c# wpf asynchronous async-await .net-core-3.1
2个回答
13
投票

遗憾的是,目前(.NET 5)用于访问文件系统的内置异步 API 并未按照 Microsoft 的“自己的建议”关于异步方法的预期行为一致地实现。

基于 TAP 的异步方法可以在返回结果任务之前同步执行少量工作,例如验证参数和启动异步操作。同步工作应保持在最低限度,以便异步方法可以快速返回。

StreamReader.ReadToEndAsync

 这样的方法不会以这种方式运行,而是在返回不完整的 
Task 之前阻塞当前线程相当长的时间。例如,在我的一个
较旧的实验
中,从 SSD 读取 6MB 文件,此方法阻塞调用线程 120 毫秒,返回一个 Task,然后仅在 20 毫秒后完成。我的建议是避免使用 GUI 应用程序中的异步文件系统 API,而使用
Task.Run
 中包含的同步 API。
var lines = await Task.Run(() => File.ReadAllLines(@"D:\temp.txt"));


更新:

以下是一些File.ReadAllLinesAsync

的实验结果:
Stopwatch stopwatch = Stopwatch.StartNew(); Task<string[]> task = File.ReadAllLinesAsync(@"C:\6MBfile.txt"); long duration1 = stopwatch.ElapsedMilliseconds; bool isCompleted = task.IsCompleted; stopwatch.Restart(); string[] lines = await task; long duration2 = stopwatch.ElapsedMilliseconds; Console.WriteLine($"Create: {duration1:#,0} msec, Task.IsCompleted: {isCompleted}"); Console.WriteLine($"Await: {duration2:#,0} msec, Lines: {lines.Length:#,0}");

输出:

Create: 450 msec, Task.IsCompleted: False Await: 5 msec, Lines: 204,000

方法
File.ReadAllLinesAsync

阻塞了当前线程450毫秒,返回的任务在5毫秒后完成。多次运行后这些测量结果是一致的。

.NET Core 3.1.3、C# 8、控制台应用程序、发布版本(未附加调试器)、Windows 10、SSD Toshiba OCZ Arc 100 240GB


.NET 6 更新。

使用 .NET 6 在相同硬件上进行相同测试: Create: 19 msec, Task.IsCompleted: False Await: 366 msec, Lines: 204,000

异步文件系统 API 的实现在 .NET 6 上得到了改进,但仍然远远落后于同步 API(它们大约是
慢 2 倍,并且不是完全异步)。所以我的建议是
使用 
Task.Run

中包含的同步 API 仍然有效。

    


0
投票

当等待异步方法时,当前线程将等待异步方法的结果。在这种情况下,当前线程是主线程,因此它等待读取过程的结果,从而冻结 UI。 (UI由主线程处理)

为了与其他用户分享更多信息,我创建了一个 Visual Studio 解决方案来实际给出想法。

问题

:异步读取大文件并处理它而不冻结 UI。

案例1

:如果很少发生,我的建议是创建一个线程并读取文件内容,处理文件然后杀死线程。使用按钮单击事件中的以下代码行。 OpenFileDialog fileDialog = new OpenFileDialog() { Multiselect = false, Filter = "All files (*.*)|*.*" }; var b = fileDialog.ShowDialog(); if (string.IsNullOrEmpty(fileDialog.FileName)) return; Task.Run(async () => { var fileContent = await File.ReadAllLinesAsync(fileDialog.FileName, Encoding.UTF8); // Process the file content label1.Invoke((MethodInvoker)delegate { label1.Text = fileContent.Length.ToString(); }); });

案例2

:如果连续发生,我的建议是创建一个频道并在后台线程中订阅它。每当发布新文件名时,消费者都会异步读取并处理它。 架构:

在构造函数中调用以下方法(

InitializeChannelReader

)来订阅频道。

private async Task InitializeChannelReader(CancellationToken cancellationToken)
{
    do
    {
        var newFileName = await _newFilesChannel.Reader.ReadAsync(cancellationToken);
        var fileContent = await File.ReadAllLinesAsync(newFileName, Encoding.UTF8);

        // Process the file content
        label1.Invoke((MethodInvoker)delegate
        {
            label1.Text = fileContent.Length.ToString();
        });
    } while (!cancellationToken.IsCancellationRequested);
}

调用method方法将文件名发布到消费者将消费的通道。使用按钮单击事件中的以下代码行。

OpenFileDialog fileDialog = new OpenFileDialog() { Multiselect = false, Filter = "All files (*.*)|*.*" }; var b = fileDialog.ShowDialog(); if (string.IsNullOrEmpty(fileDialog.FileName)) return; await _newFilesChannel.Writer.WriteAsync(fileDialog.FileName);

© www.soinside.com 2019 - 2024. All rights reserved.