何时将IEnumerable转换为IAsyncEnumerable



在Controller Action Return Types (doc link)的。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并在foreach上做await的原因是什么?这样做是为了简化语法,还是有好处?

将任何IEnumerable转换为IAsyncEnumerable,或者仅当底层IEnumerable也是可流的(例如通过yield)时,是否有好处?如果我已经有一个列表完全加载到内存中,将其转换为IAsyncEnumerable是毫无意义的吗?

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

如果您想更深入地了解发生了什么,您可以手动枚举异步序列,而不是方便的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值都是零。

首先转换为IAsyncEnumerable并在foreach上执行await的原因是什么?

如果你想利用异步I/O。无论何时你从数据库中获取数据,在IEnumerable的情况下,这是一个阻塞操作。调用线程(您的foreach)必须等待数据库响应到达。而在IAsyncEnumerable的情况下,调用线程(您的await foreach)可以分配给不同的请求。因此,它提供了更好的可伸缩性。

这样做是为了简化语法还是有好处?

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

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

在您的特定示例中,您有两个IAsyncEnumerable。您的数据源和http响应。你不必两者都用。流式传输IEnumerable是完全可以的。异步获取数据源并在所有数据可用时返回结果也是可以的。

如果我有一个列表完全加载到内存已经,是毫无意义的转换成IAsyncEnumerable?

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

相关内容

  • 没有找到相关文章

最新更新