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

ser*_*raj 4 c# wpf asynchronous async-await .net-core-3.1

这是我的代码。读取文件行的​​ 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
}
Run Code Online (Sandbox Code Playgroud)

File.ReadAllLines()在 .NET Core 3.1 WPF App 中使用了异步版本的方法。

但它阻塞了 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";
    }
Run Code Online (Sandbox Code Playgroud)

结果是:

Create: 652 msec msec, Task.IsCompleted: False | Await:   15 msec, Lines: 480,001
Run Code Online (Sandbox Code Playgroud)

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

The*_*ias 7

遗憾的是,根据 Microsoft自己关于异步方法预期行为的建议,用于访问文件系统的内置异步 API 并未一致实现。

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

StreamReader.ReadToEndAsync这样的方法不会以这种方式运行,而是在返回一个不完整的Task. 例如,在我从 SSD 读取 6MB 文件的旧实验中,此方法将调用线程阻塞了 120 毫秒,Task然后返回一个仅在 20 毫秒后完成的。我的建议是避免使用 GUI 应用程序中的异步文件系统 API,而是使用Task.Run.

var lines = await Task.Run(() => File.ReadAllLines(@"D:\temp.txt"));
Run Code Online (Sandbox Code Playgroud)

更新:以下是一些实验结果File.ReadAllLinesAsync

var stopwatch = Stopwatch.StartNew();
var task = File.ReadAllLinesAsync(@"C:\6MBfile.txt");
var duration1 = stopwatch.ElapsedMilliseconds;
bool isCompleted = task.IsCompleted;
stopwatch.Restart();
var lines = await task;
var duration2 = stopwatch.ElapsedMilliseconds;
Console.WriteLine($"Create: {duration1:#,0} msec, Task.IsCompleted: {isCompleted}");
Console.WriteLine($"Await:  {duration2:#,0} msec, Lines: {lines.Length:#,0}");
Run Code Online (Sandbox Code Playgroud)

输出:

var lines = await Task.Run(() => File.ReadAllLines(@"D:\temp.txt"));
Run Code Online (Sandbox Code Playgroud)

该方法File.ReadAllLinesAsync阻塞当前线程450毫秒,5毫秒后返回的任务完成。这些测量在多次运行后是一致的。

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

  • @aepot我用“File.ReadAllLinesAsync”的实验结果更新了我的答案。请在您的电脑中尝试此代码片段并报告您的结果。关于“Task.Run”是不好的做法,这对于 ASP.NET 应用程序来说绝对是正确的,而对于 WinForms/WPF 应用程序来说几乎不重要。保持 UI 响应能力的价值使任何有关将“ThreadPool”大小增加一两个线程的考虑相形见绌。 (3认同)
  • 使用 .NET Core 3.1 API `File.ReadAllLinesAsync` 的 OP。它确实是异步的。`Task.Run()` 在这里是不好的做法,就像在文件读取时浪费池线程一样,不建议用于基于 I/O 的操作。看来OP的问题超出了显示的代码范围。 (2认同)
  • 我已经完成调查并且:是的!`ReadAllLinesAsync` 是完全损坏的 API 方法。我有相当快的 SSD,并使用 12MB 文本文件(里面有简单的 json)进行了测试。结果让我脑子坏掉了。太棒了:“ReadAllLinesAsync”= 350 毫秒,UI 冻结,“Task.Run => ReadAllLines”(“任务”中的同步调用)- 55 毫秒。**异步** 慢 7 倍!使困惑。**最后:** [这是一个错误](https://github.com/dotnet/runtime/issues/27047)。我认为这是附加此信息和此[参考](/sf/ask/3609231041/)的答案的原因。 (2认同)
  • “ReadAllTextAsync”、“WriteAllLinesAsync”和“WriteAllTextAsync”具有相同的错误。没想到来自.NET。所有测试均在 .NET Core 3.1 上执行。 (2认同)
  • @aepot 是的,异步文件系统 API 被破坏了,这真是令人遗憾。不久前,我写了[我的论点](/sf/ask/2711758241/#58306020),赞成在中使用 `Task.Run` GUI 应用程序的事件处理程序,不包括内置异步 API,因为它们“由专家实现”。我知道的很少... (2认同)
  • @TheodorZoulias 是的,逐行流式传输 - 整体上会更慢。这里流式处理行的好处是,您可以选择一次处理一行(分配内存),这对于微服务来说非常有用:) (2认同)