【问题标题】:Can I await an enumerable I create with a generator?我可以等待使用生成器创建的可枚举项吗?
【发布时间】:2014-08-05 07:21:46
【问题描述】:

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

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 987654330@s 或作废。

现在,我意识到我可以简单地删除等待并返回 IEnumerable&lt;Task&lt;int&gt;&gt; 但这对我没有帮助,因为迭代会在任何数据准备好之前继续询问数据,所以它不能解决我的问题。

  • 有没有什么方法可以很好地将枚举和任务与语言给我的 await 和 yield 的糖很好地混合在一起?
  • 有什么方法可以很好地消耗它吗?

(通过在线搜索,我怀疑第一个问题的答案是错误的,第二个问题是观察者/可观察的,但我找不到任何规范的参考资料,我对最好的方法感兴趣用 C# 实现这个模式)

【问题讨论】:

  • 您实际上是想获得Task&lt;IEnumerable&lt;int&gt;&gt;,还是IEnumerable&lt;Task&lt;int&gt;&gt;?后者更容易实现......但目前尚不清楚您实际上想要摆脱什么。
  • (顺便说一句,我明白在这种情况下表达你想要实现的目标可能非常困难。)
  • @JonSkeet 在听起来很愚蠢的机会上,我认为我正在尝试获得Task&lt;IEnumerable&lt;Task&lt;int&gt;&gt;&gt;,我想获得一个我会等待的迭代器最初,然后在每次迭代时等待。但是,IEnumerable&lt;Task&lt;int&gt;&gt; 也是可以接受的,问题是正如我在问题中提到的那样,IEnumerable 将在消费者身上同步消耗,我想在每次迭代中等待它,然后再消耗下一个元素。我想知道是否有一种聪明的方法可以做到这一点,它不涉及常规的 for 循环,在那里 await 暂停迭代。
  • @BenjaminGruenbaum, this question 密切相关。 AFAICT,你想要做的是一个很棒的 C# 语言 + LINQ 运行时特性,但它还不可用。另请查看 Stephen Toub 的 "Tasks, Monads, and LINQ"
  • 您正在寻找类似 @​​987654337@ 的东西,它公开了每个元素的异步性。

标签: c# asynchronous async-await generator promise


【解决方案1】:

异步序列很有趣。有许多不同的方法,具体取决于您想要做什么。我对你想要的语义并不完全清楚,所以这些是一些选项。

Task&lt;IEnumerable&lt;T&gt;&gt; 是一个异步检索的集合。只有一个任务——一个异步操作——检索整个集合。这听起来不像你想要的。

IEnumerable&lt;Task&lt;T&gt;&gt; 是(异步)数据的(同步)序列。有多个任务,可能会或可能不会同时处理。有几个选项可以实现这一点。一种是使用枚举器块并产生任务;每次从可枚举中检索下一项时,这种方法都会启动一个新的异步操作。或者,您可以创建并返回所有任务同时运行的任务集合(这可以通过 LINQ 的 Select 后跟 ToList/ToArray 在源序列上优雅地完成)。但是,这有两个缺点:无法异步确定序列是否已经结束,并且在返回当前项后立即开始下一个项处理并不容易(这通常是期望的行为)。

核心问题是IEnumerable&lt;T&gt;本质上是同步的。有几种解决方法。一个是IAsyncEnumerable&lt;T&gt;,它是IEnumerable&lt;T&gt; 的异步等效项,在Ix-Async NuGet package 中可用。但是,这种方法有其自身的缺点。当然,您将失去对IEnumerable&lt;T&gt;(即枚举器块和foreach)的良好语言支持。此外,“异步可枚举”的概念并不完全是高性能的。理想情况下,异步 API 应该是笨重的而不是健谈的,并且枚举是非常健谈的。有关original design herechunky/chatty considerations here 的更多讨论。

因此,如今更常见的解决方案是使用 observablesdataflows(两者都可以通过 NuGet 获得)。在这些情况下,您必须将“序列”视为具有自己生命的事物。 Observables 是基于推送的,因此消费代码(理想情况下)是反应式的。数据流具有演员的感觉,因此它们的行为更加独立,再次将结果推送到消费代码。

【讨论】:

    【解决方案2】:

    您可以返回IEnumerable&lt;Task&lt;int&gt;&gt;

    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);
    

    【讨论】:

    • 请注意,消费者在请求下一个任务之前不需要等待任务完成,所以如果生成器在并行请求多个项目时无法正常工作,它可能有问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-11-09
    • 2015-03-02
    • 1970-01-01
    • 2021-03-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多