这是我的代码。读取文件行的 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
遗憾的是,目前(.NET 5)用于访问文件系统的内置异步 API 并未按照 Microsoft 的“自己的建议”关于异步方法的预期行为一致地实现。
基于 TAP 的异步方法可以在返回结果任务之前同步执行少量工作,例如验证参数和启动异步操作。同步工作应保持在最低限度,以便异步方法可以快速返回。像
这样的方法不会以这种方式运行,而是在返回不完整的
Task
之前阻塞当前线程相当长的时间。例如,在我的一个较旧的实验中,从 SSD 读取 6MB 文件,此方法阻塞调用线程 120 毫秒,返回一个
Task
,然后仅在 20 毫秒后完成。我的建议是避免使用 GUI 应用程序中的异步文件系统 API,而使用 Task.Run
中包含的同步 API。
var lines = await Task.Run(() => File.ReadAllLines(@"D:\temp.txt"));
的实验结果:
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 在相同硬件上进行相同测试:
Create: 19 msec, Task.IsCompleted: False
Await: 366 msec, Lines: 204,000
异步文件系统 API 的实现在 .NET 6 上得到了改进,但仍然远远落后于同步 API(它们大约是 慢 2 倍,并且不是完全异步)。所以我的建议是 使用
Task.Run
中包含的同步 API 仍然有效。
当等待异步方法时,当前线程将等待异步方法的结果。在这种情况下,当前线程是主线程,因此它等待读取过程的结果,从而冻结 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();
});
});
:如果连续发生,我的建议是创建一个频道并在后台线程中订阅它。每当发布新文件名时,消费者都会异步读取并处理它。 架构:
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);