提问人:eriyg 提问时间:3/21/2022 最后编辑:Theodor Zouliaseriyg 更新时间:3/21/2022 访问量:277
为什么 C# 中的异步 IO 会阻塞?[复制]
Why does async IO block in C#? [duplicate]
问:
我创建了一个 WPF 应用程序,它以本地文档数据库为目标,用于娱乐/练习。这个想法是实体的文档是一个.json文件,它位于磁盘上,文件夹充当集合。在这个实现中,我有一堆.json文档,它们提供有关视频的数据,以创建一种IMDB克隆。
我有这个类:
public class VideoRepository : IVideoRepository
{
public async IAsyncEnumerable<Video> EnumerateEntities()
{
foreach (var file in new DirectoryInfo(Constants.JsonDatabaseVideoCollectionPath).GetFiles())
{
var json = await File.ReadAllTextAsync(file.FullName); // This blocks
var document = JsonConvert.DeserializeObject<VideoDocument>(json); // Newtonsoft
var domainObject = VideoMapper.Map(document); // A mapper to go from the document type to the domain type
yield return domainObject;
}
// Uncommenting the below lines and commenting out the above foreach loop doesn't lock up the UI.
//await Task.Delay(5000);
//yield return new Video();
}
// Rest of class.
}
在调用堆栈中,通过 API 层进入 UI 层,我在 ViewModel 中有一个 ICommand:
QueryCommand = new RelayCommand(async (query) => await SendQuery((string)query));
private async Task SendQuery(string query)
{
QueryStatus = "Querying...";
QueryResult.Clear();
await foreach (var video in _videoEndpoints.QueryOnTags(query))
QueryResult.Add(_mapperService.Map(video));
QueryStatus = $"{QueryResult.Count()} videos found.";
}
目标是向用户显示一条消息“正在查询...”在处理查询时。但是,该消息永远不会显示,并且 UI 会锁定,直到查询完成,此时将显示结果消息。
在 VideoRepository 中,如果我注释掉 foreach 循环并取消注释它下面的两行,UI 不会锁定并且“Querying...”消息显示 5 秒钟。
为什么会这样?有没有办法在不锁定 UI/阻止的情况下进行 IO?
幸运的是,如果这是在 Web API 后面并访问了真正的数据库,我可能不会看到这个问题。不过,我仍然希望 UI 不要锁定这个实现。
编辑: 为什么 File.ReadAllLinesAsync() 会阻止 UI 线程?
事实证明,Microsoft并没有使他们的异步方法非常异步。更改 IO 线路可以解决所有问题:
//var json = await File.ReadAllTextAsync(file.FullName); // Bad
var json = await Task.Run(() => File.ReadAllText(file.FullName)); // Good
答:
你的目标可能是早于 .NET 6 的 .NET 版本。在这些旧版本中,文件系统 API 没有有效地实现,甚至不是真正的异步。.NET 6 中的内容已得到改进,但同步文件系统 API 的性能仍然高于异步 API。您的问题可以通过简单地从以下位置切换来解决:
var json = await File.ReadAllTextAsync(file.FullName);
对此:
var json = await Task.Run(() => File.ReadAllText(file.FullName));
如果你想变得花哨,你也可以通过使用自定义 LINQ 运算符来解决 UI 层中的问题,如下所示:
public static async IAsyncEnumerable<T> OnThreadPool<T>(
this IAsyncEnumerable<T> source,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var enumerator = await Task.Run(() => source
.GetAsyncEnumerator(cancellationToken)).ConfigureAwait(false);
try
{
while (true)
{
var (moved, current) = await Task.Run(async () =>
{
if (await enumerator.MoveNextAsync())
return (true, enumerator.Current);
else
return (false, default);
}).ConfigureAwait(false);
if (!moved) break;
yield return current;
}
}
finally
{
await Task.Run(async () => await enumerator
.DisposeAsync()).ConfigureAwait(false);
}
}
此运算符卸载到与枚举 .它可以像这样使用:ThreadPool
IAsyncEnumerable<T>
await foreach (var video in _videoEndpoints.QueryOnTags(query).OnThreadPool())
QueryResult.Add(_mapperService.Map(video));
评论