我可以等待我用发电机创建的可枚举数吗?

时间:2014-06-15 06:50:01

标签: c# asynchronous async-await generator promise

假设我有异步获得的整数序列。

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#中实现这种模式

2 个答案:

答案 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 herechunky/chatty considerations here

的更多讨论

所以,现在更常见的解决方案是使用observablesdataflows(两者也可通过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);