在控制器操作返回类型的 .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
是没有意义的吗?
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
值。
首先转换为 IAsyncEnumerable 并在 foreach 上等待的原因是什么?
如果你想利用异步 I/O。每当您从数据库中获取数据时,
IEnumerable
都是阻塞操作。调用线程(你的foreach
)必须等到数据库响应到达。而在 IAsyncEnumerable
的情况下,调用者线程(您的 await foreach
)可以分配给不同的请求。因此,它提供了更好的可扩展性。
只是为了更简单的语法还是这样做有好处?
如果下一个项目(很可能)还不可用,并且您需要执行 I/O 操作来获取它,那么您可以同时释放(否则阻塞)调用者线程。
将任何 IEnumerable 转换为 IAsyncEnumerable 是否有好处,或者仅当底层 IEnumerable 也是可流式传输时(例如通过 yield)?
在您的特定示例中,您有两个
IAsyncEnumerable
s。您的数据源和您的 http 响应。您不必同时使用两者。直播一个IEnumerable
绝对没问题。也可以异步获取数据源并在所有数据可用时返回结果。
如果我已经将一个列表完全加载到内存中,将其转换为 IAsyncEnumerable 是否毫无意义?
好吧,如果您想流式传输响应,那么是的,不需要异步获取数据源。