何时将 IEnumerable 转换为 IAsyncEnumerable

问题描述 投票:0回答:2

在控制器操作返回类型的 .NET 文档中(文档链接),它显示了有关如何返回异步响应流的示例:

[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
    var products = _productContext.Products.OrderBy(p => p.Name).AsAsyncEnumerable();

    await foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}

在上面的示例中,

_productContext.Products.OrderBy(p => p.Name).AsAsyncEnumerable()
将返回的
IQueryable<Product>
转换为
IAsyncEnumerable
。但是下面的示例也可以工作并异步流式传输响应。

[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
    var products = _productContext.Products.OrderBy(p => p.Name);

    foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }

    await Task.CompletedTask;
}

先转换成

IAsyncEnumerable
然后在
await
上做
foreach
的原因是什么?仅仅是为了更简单的语法还是这样做有好处?

将任何

IEnumerable
转换为
IAsyncEnumerable
是否有好处,或者仅当基础
IEnumerable
也可流式传输时,例如通过
yield
?如果我已经将一个列表完全加载到内存中,将其转换为
IAsyncEnumerable
是没有意义的吗?

c# async-await asp.net-core-webapi ienumerable iasyncenumerable
2个回答
1
投票

IAsyncEnumerable<T>
相对于
IEnumerable<T>
的好处是前者可能更具可扩展性,因为它在枚举时不使用线程。它没有同步
MoveNext
方法,而是异步
MoveNextAsync
方法。当
MoveNextAsync
总是返回一个已经完成的
ValueTask<bool>
(
enumerator.MoveNextAsync().IsCompleted == true
) 时,这个好处成为一个有争议的点,在这种情况下,您只有一个伪装成异步的同步枚举。在这种情况下没有可扩展性优势。这正是问题中显示的代码中发生的事情。您拥有保时捷的底盘,引擎盖下隐藏着 Trabant 发动机。

如果你想更深入地了解到底是怎么回事,可以手动枚举异步序列而不是方便的

await foreach
,并收集枚举每一步的调试信息:

[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
    var products = _productContext.Products.OrderBy(p => p.Name).AsAsyncEnumerable();

    Stopwatch stopwatch = new();
    await using IAsyncEnumerator<Product> enumerator = products.GetAsyncEnumerator();
    while (true)
    {
        stopwatch.Restart();
        ValueTask<bool> moveNextTask = enumerator.MoveNextAsync();
        TimeSpan elapsed1 = stopwatch.Elapsed;
        bool isCompleted = moveNextTask.IsCompleted;
        stopwatch.Restart();
        bool moved = await moveNextTask;
        TimeSpan elapsed2 = stopwatch.Elapsed;
        Console.WriteLine($"Create: {elapsed1}, Completed: {isCompleted}, Await: {elapsed2}");
        if (!moved) break;

        Product product = enumerator.Current;
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}

您很可能会发现所有

MoveNextAsync
操作在创建时就已完成,至少其中一些操作具有重要的
elapsed1
值,并且所有操作都具有零
elapsed2
值。


0
投票

首先转换为 IAsyncEnumerable 并在 foreach 上等待的原因是什么?

如果你想利用异步 I/O。每当您从数据库中获取数据时,

IEnumerable
都是阻塞操作。调用线程(你的
foreach
)必须等到数据库响应到达。而在
IAsyncEnumerable
的情况下,调用者线程(您的
await foreach
)可以分配给不同的请求。因此,它提供了更好的可扩展性。

只是为了更简单的语法还是这样做有好处?

如果下一个项目(很可能)还不可用,并且您需要执行 I/O 操作来获取它,那么您可以同时释放(否则阻塞)调用者线程。

将任何 IEnumerable 转换为 IAsyncEnumerable 是否有好处,或者仅当底层 IEnumerable 也是可流式传输时(例如通过 yield)?

在您的特定示例中,您有两个

IAsyncEnumerable
s。您的数据源和您的 http 响应。您不必同时使用两者。直播一个
IEnumerable
绝对没问题。也可以异步获取数据源并在所有数据可用时返回结果。

如果我已经将一个列表完全加载到内存中,将其转换为 IAsyncEnumerable 是否毫无意义?

好吧,如果您想流式传输响应,那么是的,不需要异步获取数据源。

© www.soinside.com 2019 - 2024. All rights reserved.