假设我有异步获得的整数序列。
async Task<int> GetI(int i){
return await Task.Delay(1000).ContinueWith(x => i);
}
我想在该序列上创建一个生成器,如果序列是同步的,我会这样做:
IEnumerable<int> Method()
{
for (var i = 0; i < 100; i++)
{
yield return GetI(i); // won't work, since getI returns a task
}
}
所以,我认为类比是让生成器异步并从中产生:
async Task<IEnumerable<int>> Method()
{
for (var i = 0; i < 100; i++)
{
yield return await Task.Delay(1000).ContinueWith(x => i);
}
}
这不起作用,因为yield
的方法必须返回IEnumerable
的某个东西,替代方案,更有意义的是IEnumerable<Task<int>>
但是自{{ {1}}方法必须返回async
或无效。
现在,我意识到我可以简单地删除await并返回一个Task
但这对我没有帮助,因为迭代会在任何数据准备就绪之前继续询问数据,所以它不能解决我的问题问题。
(从在线搜索,我怀疑第一个问题的答案是错误的,第二个是观察者/可观察者,但我找不到任何规范参考,我对最好的方法感兴趣在C#中实现这种模式
答案 0 :(得分:13)
异步序列很有趣。根据您的具体要求,有许多不同的方法。我并不完全清楚你想要的语义,所以这些是一些选择。
Task<IEnumerable<T>>
是一个异步检索的集合。只有一个任务 - 一个异步操作 - 检索整个集合。这听起来并不像你想要的那样。
IEnumerable<Task<T>>
是(异步)数据的(同步)序列。有多个任务,可能同时也可能不同时处理。有几种方法可以实现这一点。一个是使用枚举器块并产生任务;每次从枚举中检索下一个项目时,此方法将启动一个新的异步操作。或者,您可以创建并返回一组任务,其中所有任务同时运行(这可以通过LINQ的Select
后跟ToList
/ ToArray
优先于源序列完成。但是,这有一些缺点:没有办法异步确定序列是否已经结束,并且在返回当前项目后立即启动 next 项处理并不容易(这通常是期望的行为)。
核心问题是IEnumerable<T>
本质上是同步的。有几种解决方法。一个是IAsyncEnumerable<T>
,它是IEnumerable<T>
的异步等价物,可在Ix-Async NuGet package中使用。不过,这种方法有其自身的缺点。当然,你失去了IEnumerable<T>
的良好语言支持(即枚举器块和foreach
)。而且,“异步可枚举”的概念并不完全相同;理想情况下,异步API应该是厚实的而不是繁琐的,并且可用的数据非常繁琐。有关original design here和chunky/chatty considerations here。
所以,现在更常见的解决方案是使用observables或dataflows(两者也可通过NuGet获得)。在这些情况下,你必须将“序列”视为具有自己生命的东西。 Observable是基于推送的,因此消费代码(理想情况下)是反应性的。数据流具有演员感觉,因此它们更独立,再次将结果推送到消费代码。
答案 1 :(得分:3)
您可以返回IEnumerable<Task<int>>
:
IEnumerable<Task<int>> Method()
{
for (var i = 0; i < 100; i++)
{
yield return Task.Delay(1000).ContinueWith(x => i);
}
}
并称之为:
foreach(var i in Method())
Console.WriteLine(await i);