使用 C# 7 特性异步返回集合的多种方法

8

我有一个简单的同步方法,看起来像这样:

public IEnumerable<Foo> MyMethod(Source src)
{
    // returns a List of Oof objects from a web service
    var oofs = src.LoadOofsAsync().Result; 
    foreach(var oof in oofs)
    {
         // transforms an Oof object to a Foo object
         yield return Transform(oof); 
    }
}

由于该方法是Web应用程序的一部分,因此最好尽可能有效地使用所有资源。因此,我希望将该方法更改为异步方法。最简单的选项是执行以下操作:

public async Task<IEnumerable<Foo>> MyMethodAsync(Source src)
{
    var oofs = await src.LoadOofsAsync();
    return oofs.Select(oof => Transform(oof));
}

我不是async/awaitIEnumerable的专家。然而,据我所知,使用这种方法会“破坏”IEnumerable的优点,因为任务等待整个集合加载,从而省略了IEnumerable集合的“惰性”。
在其他StackOverflow帖子中,我读到了几个建议使用Rx.NET(或System.Reactive)。快速浏览文档后,我发现IObservable<T>是它们的异步替代方案IEnumerable<T>。然而,尝试使用幼稚的方法并输入以下内容并没有起作用:
public async IObservable<Foo> MyMethodReactive(Source src)
{
    var oofs = await src.LoadOofsAsync();
    foreach(var oof in oofs)
    {
        yield return Transform(oof);
    }
}

我遇到了编译错误,即 IObservable<T> 既没有实现 GetEnumerator() 也没有实现 GetAwaiter() - 因此它无法同时使用 yieldasync。我没有深入阅读 Rx.NET 的文档,所以我可能只是错误地使用了该库。但我不想花时间学习一个新框架来修改一个方法。

有了 C# 7 中的新 功能, 现在可以实现自定义类型。因此,理论上,我可以实现一个 IAsyncEnumerable,它将定义两个方法 GetEnumerator()GetAwaiter()。然而,从我的以往经验中,我记得曾尝试创建一个自定义的 GetEnumerator() 实现... 最终我得到了一个隐藏在容器中的简单列表。

因此,我们有四种可能的方法来解决这个任务:

  1. 保持代码同步,但使用 IEnumerable
  2. 将其改为异步,但在 Task<T> 中包装 IEnumerable
  3. 学习并使用 Rx.NET(System.Reactive)
  4. 使用 C# 7 特性创建自定义的 IAsyncEnumerable

这些尝试的优点和缺点是什么?哪一个对资源利用有最显著的影响?


@AdrianoRepetti,虽然AsParallel适用于I/O绑定任务吗? - user585968
3
如果你担心损失效益,你真正应该担心的是src.LoadOofsAsync(),它已经返回了一个Task<IEnumerable<Oof>>。再次包装异步操作并不能帮助解决问题,除非Transform确实很耗费资源,并且可以有意义地并行化处理/等待。 - Jeroen Mostert
2
@Magnus:我的观点是,无论采用什么技术来使MyMethod“好”/“更好”,最好也应该首先应用于LoadOofsAsync,或者至少在那里进行考虑。真正流式处理的解决方案与将初始处理时间减半之间存在很大的区别(尽管这当然不是无用的)。 - Jeroen Mostert
没有单一的固定模式,因为它取决于您的需求。特别是应用 Observable(这是最接近完全异步枚举的东西)对代码库的其余部分有相当大的影响(更不用说由此带来的学习曲线)。一些项目实现了具有 MoveNextAsyncIAsyncEnumerator,但缺点是不支持所有现有的枚举操作(甚至是像 foreach 这样简单的操作)。这是在简单和最大响应/酷炫之间的平衡。 - Jeroen Mostert
@Magnus 目前还没有。 但是,这种“异步化”是更大过程的一部分,我们的团队正在调查使用WebSockets、PUSH服务和几个IoT协议的可能性。 我们的目标是研究可用技术并选择最好的技术,以便其他团队在其他产品中使用。 现在,我们也正在研究SignalR 3。 如果它能够逐个发送IEnumerable中的项,则惰性也将成为一个重要因素。 - lss
显示剩余5条评论
1个回答

2
在您的情况下,最好的选择似乎是Task<IEnumerable<T>>。以下是每个选项的优点和缺点:
  1. 同步代码(或并行同步代码)在没有I/O但需要大量CPU使用时表现出色。如果您有等待同步的I/O代码(例如第一个方法实现),那么CPU只会在等待Web服务响应时烧掉周期而不做任何事情。

  2. Task<IEnumerable<T>>用于获取集合的I/O操作。正在等待I/O操作的线程可以在等待时安排其他任务。这听起来像是您的情况。

  3. Rx最适合推送场景:当数据被“推送”到您的代码中并且您希望对其进行响应时。常见的例子是接收股票市场定价数据或聊天应用程序。

  4. IAsyncEnumerable用于当您有一个集合,其中每个项目都需要或生成异步任务的情况。例如:迭代一组项目并为每个项目执行某种独特的DB查询。如果您的Transform实际上是一个I/O绑定的异步方法,则这可能更明智。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接