为啥 File.ReadAllLinesAsync() 会阻塞 UI 线程?
Posted
技术标签:
【中文标题】为啥 File.ReadAllLinesAsync() 会阻塞 UI 线程?【英文标题】:Why File.ReadAllLinesAsync() blocks the UI thread?为什么 File.ReadAllLinesAsync() 会阻塞 UI 线程? 【发布时间】:2021-08-29 18:35:27 【问题描述】:这是我的代码。用于读取文件行的 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 App 中使用了异步版本的 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
【问题讨论】:
这能回答你的问题吗? How to Async Files.ReadAllLines and await for results? 你对正在阅读的文本文件做了什么,它有多大?例如,如果您在读取文件内容后立即将文本添加到文本框,则会在填充文本框时阻塞 UI 线程。 调用本身不会阻塞。通话结束后将所有内容注释掉,然后自己查看。 我的建议 - 按照答案中的建议使用var lines = await Task.Run(() => File.ReadAllLines(@"D:\temp.txt"));
,它不仅可以保持 UI 响应,而且 7x faster 比 ReadAllLinesAsync
更好!
@aepot 您可以尝试稍微改进一下问题,例如修复大小写并正确格式化代码。这将增加该问题被重新投票的可能性(需要再投票一次)。
【参考方案1】:
遗憾的是,目前 (.NET 5) 用于访问文件系统的内置异步 API 并未根据 Microsoft 的 own recommendations 关于异步方法预期行为的一致实现。
基于 TAP 的异步方法可以在返回结果任务之前同步执行少量工作,例如验证参数和启动异步操作。应将同步工作保持在最低限度,以便异步方法可以快速返回。
StreamReader.ReadToEndAsync
之类的方法不会以这种方式运行,而是在返回不完整的Task
之前将当前线程阻塞相当长的时间。例如,在我的older experiment 从我的 SSD 读取 6MB 文件时,此方法将调用线程阻塞 120 毫秒,返回一个 Task
,然后仅在 20 毫秒后完成。我的建议是避免使用来自 GUI 应用程序的异步文件系统 API,而是使用包装在 Task.Run
中的同步 API。
var lines = await Task.Run(() => File.ReadAllLines(@"D:\temp.txt"));
更新:以下是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");
输出:
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 仍然有效。
【讨论】:
OP 使用 .NET Core 3.1 APIFile.ReadAllLinesAsync
。它是真正的异步。 Task.Run()
在这里是不好的做法,就像在苍蝇读取时浪费池线程一样,不建议用于基于 I/O 的操作。似乎 OP 的问题不在显示的代码范围内。
@aepot 我用File.ReadAllLinesAsync
的实验结果更新了我的答案。请在您的 PC 中尝试此代码 sn-p 并报告您的结果。关于Task.Run
是不好的做法,这对于 ASP.NET 应用程序绝对正确,对于 WinForms/WPF 应用程序几乎不重要。保持 UI 完全响应的价值使任何关于将ThreadPool
的大小增加一两个线程的考虑都相形见绌。
我已经完成了调查并且:是的! ReadAllLinesAsync
完全是 BROKEN API 方法。我有相当快的 SSD 并用 12MB 的文本文件(里面有简单的 json)进行了测试。结果让我的大脑崩溃了。太棒了:ReadAllLinesAsync
= 350 毫秒,UI 冻结,Task.Run => ReadAllLines
(同步调用 Task
) - 55 毫秒。 Async 慢了 7 倍!使困惑。 最后: here's a Bug。我认为这是使用此信息和此reference 附加答案的原因。
ReadAllTextAsync
、WriteAllLinesAsync
和 WriteAllTextAsync
的相同错误。没想到来自.NET。所有测试均在 .NET Core 3.1 上进行。
@aepot 是的,很遗憾异步文件系统 API 被破坏了。前段时间我写了my arguments,赞成在GUI 应用程序的事件处理程序中使用Task.Run
,不包括内置的异步API,因为它们“由专家实现”。我几乎不知道...【参考方案2】:
感谢 Theodor Zoulias 的回答,它是正确且有效的。
在等待异步方法时,当前线程会等待异步方法的结果。在这种情况下,当前线程是主线程,所以它等待读取过程的结果,从而冻结 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);
调用方法方法,以便将文件名发布到将由消费者消费的频道。使用按钮的点击事件中的以下代码行。
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);
【讨论】:
以上是关于为啥 File.ReadAllLinesAsync() 会阻塞 UI 线程?的主要内容,如果未能解决你的问题,请参考以下文章
为啥使用 glTranslatef?为啥不直接更改渲染坐标?
为啥 DataGridView 上的 DoubleBuffered 属性默认为 false,为啥它受到保护?