在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?
好吧,如果你想流式响应,那么是的,它不需要异步获取数据源。