为什么 C# 中的异步 IO 会阻塞?[复制]

Why does async IO block in C#? [duplicate]

提问人:eriyg 提问时间:3/21/2022 最后编辑:Theodor Zouliaseriyg 更新时间:3/21/2022 访问量:277

问:

我创建了一个 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
C# WPF 异步 IO iaSynceNumerable

评论

1赞 Theodor Zoulias 3/21/2022
可能与此有关:为什么 File.ReadAllLinesAsync() 会阻止 UI 线程?
1赞 eriyg 3/21/2022
@TheodorZoulias经典Microsoft!将 IO 行更改为 var json = await Task.Run(() => File.ReadAllText(file.全名));修复一切。谢谢你指出这一点。

答:

2赞 Theodor Zoulias 3/21/2022 #1

你的目标可能是早于 .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);
    }
}

此运算符卸载到与枚举 .它可以像这样使用:ThreadPoolIAsyncEnumerable<T>

await foreach (var video in _videoEndpoints.QueryOnTags(query).OnThreadPool())
    QueryResult.Add(_mapperService.Map(video));

评论

1赞 eriyg 3/21/2022
谢谢。我绝对不会做UI层解决方案,最好解决上游问题。不过,这是一个很好的技巧。一些我无法更改的黑匣子有一天可能会遇到同样的问题。
0赞 Theodor Zoulias 3/21/2022
@eriyg 我也不认为我会使用它,尽管根据这篇文章,这可能是正确的做法。