为什么在 C# 中异步 IO 会阻塞?
Why does async IO block in C#?
我创建了一个 WPF 应用程序,它针对 fun/practice 的本地文档数据库。这个想法是实体的文档是一个位于磁盘上的 .json 文件,文件夹充当集合。在此实现中,我有一堆 .json 文档提供有关视频的数据以创建一种 IMDB 克隆。
我有这个class:
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.";
}
目标是在处理查询时向用户显示消息 'Querying...'。但是,该消息永远不会显示,并且 UI 会锁定,直到查询完成,此时会显示结果消息。
在 VideoRepository 中,如果我注释掉 foreach 循环并取消注释下面的两行,UI 不会锁定并且 'Querying...' 消息会显示 5 秒。
为什么会这样?有没有办法在不锁定 UI/blocking 的情况下进行 IO?
幸运的是,如果这是在网络 API 之后访问真实数据库,我可能不会看到这个问题。不过,我仍然希望 UI 不要锁定此实现。
编辑:
的骗局
事实证明,Microsoft 并没有使他们的异步方法非常异步。更改 IO 行可以解决所有问题:
//var json = await File.ReadAllTextAsync(file.FullName); // Bad
var json = await Task.Run(() => File.ReadAllText(file.FullName)); // Good
您的目标可能是早于 .NET 6 的 .NET 版本。在这些旧版本中,file-system API 没有有效地实现,. Things have been improved 在 .NET 6 中,但仍然是同步的file-system API 的性能比异步 API 更高。您的问题可以简单地通过切换来解决:
var json = await File.ReadAllTextAsync(file.FullName);
对此:
var json = await Task.Run(() => File.ReadAllText(file.FullName));
如果你想花哨一点,你也可以在 UI 层解决问题,方法是使用像这样的自定义 LINQ 运算符:
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);
}
}
此运算符将与枚举 IAsyncEnumerable<T>
相关的所有操作卸载到 ThreadPool
。可以这样使用:
await foreach (var video in _videoEndpoints.QueryOnTags(query).OnThreadPool())
QueryResult.Add(_mapperService.Map(video));
我创建了一个 WPF 应用程序,它针对 fun/practice 的本地文档数据库。这个想法是实体的文档是一个位于磁盘上的 .json 文件,文件夹充当集合。在此实现中,我有一堆 .json 文档提供有关视频的数据以创建一种 IMDB 克隆。
我有这个class:
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.";
}
目标是在处理查询时向用户显示消息 'Querying...'。但是,该消息永远不会显示,并且 UI 会锁定,直到查询完成,此时会显示结果消息。
在 VideoRepository 中,如果我注释掉 foreach 循环并取消注释下面的两行,UI 不会锁定并且 'Querying...' 消息会显示 5 秒。
为什么会这样?有没有办法在不锁定 UI/blocking 的情况下进行 IO?
幸运的是,如果这是在网络 API 之后访问真实数据库,我可能不会看到这个问题。不过,我仍然希望 UI 不要锁定此实现。
编辑:
事实证明,Microsoft 并没有使他们的异步方法非常异步。更改 IO 行可以解决所有问题:
//var json = await File.ReadAllTextAsync(file.FullName); // Bad
var json = await Task.Run(() => File.ReadAllText(file.FullName)); // Good
您的目标可能是早于 .NET 6 的 .NET 版本。在这些旧版本中,file-system API 没有有效地实现,
var json = await File.ReadAllTextAsync(file.FullName);
对此:
var json = await Task.Run(() => File.ReadAllText(file.FullName));
如果你想花哨一点,你也可以在 UI 层解决问题,方法是使用像这样的自定义 LINQ 运算符:
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);
}
}
此运算符将与枚举 IAsyncEnumerable<T>
相关的所有操作卸载到 ThreadPool
。可以这样使用:
await foreach (var video in _videoEndpoints.QueryOnTags(query).OnThreadPool())
QueryResult.Add(_mapperService.Map(video));