为什么 File.ReadAllLinesAsync() 会阻塞 UI 线程?
Why File.ReadAllLinesAsync() blocks the UI thread?
这是我的代码。读取文件行的 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
遗憾的是,目前 (.NET 5) 用于访问文件系统的内置异步 API 并未根据 Microsoft own recommendations 关于异步方法预期行为方式的一致实施。
An asynchronous method that is based on TAP can do a small amount of work synchronously, such as validating arguments and initiating the asynchronous operation, before it returns the resulting task. Synchronous work should be kept to the minimum so the asynchronous method can return quickly.
类似StreamReader.ReadToEndAsync
do not behave this way, and instead block the current thread for a considerable amount of time before returning an incomplete Task
. For example in an of mine with reading a 6MB file from my SSD, this method blocked the calling thread for 120 msec, returning a Task
that was then completed after only 20 msec. My suggestion is to avoid using the asynchronous filesystem APIs from GUI applications, and use instead the synchronous APIs wrapped in Task.Run
的方法。
var lines = await Task.Run(() => File.ReadAllLines(@"D:\temp.txt"));
更新: 以下是 File.ReadAllLinesAsync
:
的一些实验结果
var stopwatch = Stopwatch.StartNew();
var task = File.ReadAllLinesAsync(@"C:MBfile.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 仍然有效。
感谢 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);
}
调用 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);
这是我的代码。读取文件行的 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
遗憾的是,目前 (.NET 5) 用于访问文件系统的内置异步 API 并未根据 Microsoft own recommendations 关于异步方法预期行为方式的一致实施。
An asynchronous method that is based on TAP can do a small amount of work synchronously, such as validating arguments and initiating the asynchronous operation, before it returns the resulting task. Synchronous work should be kept to the minimum so the asynchronous method can return quickly.
类似StreamReader.ReadToEndAsync
do not behave this way, and instead block the current thread for a considerable amount of time before returning an incomplete Task
. For example in an Task
that was then completed after only 20 msec. My suggestion is to avoid using the asynchronous filesystem APIs from GUI applications, and use instead the synchronous APIs wrapped in Task.Run
的方法。
var lines = await Task.Run(() => File.ReadAllLines(@"D:\temp.txt"));
更新: 以下是 File.ReadAllLinesAsync
:
var stopwatch = Stopwatch.StartNew();
var task = File.ReadAllLinesAsync(@"C:MBfile.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 仍然有效。
感谢 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);
}
调用 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);