为什么要使用Task 超过ValueTask 在C#?

问题描述 投票:120回答:4

从C#7.0开始,异步方法可以返回ValueTask <T>。解释说,当我们有缓存结果或通过同步代码模拟异步时,应该使用它。但是我仍然不明白使用ValueTask的问题是什么,或者实际上为什么async / await不是从一开始就使用值类型构建的。 ValueTask何时无法完成这项工作?

c# asynchronous
4个回答
183
投票

来自the API docs(重点补充):

方法可能会返回此值类型的实例,因为它们的操作结果很可能是同步可用的,并且当预期方法被频繁调用时,为每个调用分配新的Task<TResult>的成本将会过高。

使用ValueTask<TResult>而不是Task<TResult>需要权衡利弊。例如,虽然ValueTask<TResult>可以帮助避免在成功结果同步可用的情况下进行分配,但它也包含两个字段,而Task<TResult>作为参考类型是单个字段。这意味着方法调用最终会返回两个字段而不是一个字段,这是要复制的更多数据。这也意味着如果在async方法中等待返回其中一个的方法,则由于需要存储两个字段而不是单个引用的结构,该async方法的状态机将更大。

此外,对于除了通过await消耗异步操作的结果之外的用途,ValueTask<TResult>可以导致更复杂的编程模型,这反过来可以实际上导致更多的分配。例如,考虑一种方法,该方法可以返回带有缓存任务的Task<TResult>作为常见结果或ValueTask<TResult>。如果结果的消费者希望将其用作Task<TResult>,例如在Task.WhenAllTask.WhenAny这样的方法中使用,那么首先需要使用ValueTask<TResult>Task<TResult>转换为AsTask,从而导致可以避免的分配如果首先使用缓存的Task<TResult>

因此,任何异步方法的默认选择应该是返回TaskTask<TResult>。只有当性能分析证明它值得时,才应使用ValueTask<TResult>而不是Task<TResult>


83
投票

但是我仍然不明白使用ValueTask的问题是什么

结构类型不是免费的。复制大于引用大小的结构可能比复制引用要慢。存储大于引用的结构比存储引用需要更多的内存。当可以注册引用时,可能不会注册大于64位的结构。降低收集压力的好处可能不会超过成本。

应该通过工程学科来解决性能问题。制定目标,根据目标衡量进度,然后决定如果不满足目标,如何修改程序,一路测量以确保您的更改实际上是改进。

为什么async / await不是从一开始就使用值类型构建的。

await类型已经存在之后,Task<T>被添加到C#中。如果已经存在新类型,那么发明新类型会有些不正常。 await经过了大量的设计迭代,然后才决定2012年出货的那个。完美是善的敌人;更好地提供适用于现有基础架构的解决方案,然后如果有用户需求,请在以后提供改进。

我还注意到,允许用户提供的类型作为编译器生成方法的输出的新功能增加了相当大的风险和测试负担。当您可以返回的唯一内容无效或任务时,测试团队不必考虑返回某些绝对疯狂类型的任何情况。测试编译器意味着不仅要弄清楚人们可能编写什么程序,还要编写可编写的程序,因为我们希望编译器编译所有合法程序,而不仅仅是所有合理的程序。这太贵了。

有人可以解释ValueTask何时无法完成这项工作?

事物的目的是提高性能。如果不能显着提高性能,它就无法完成工作。它无法保证。


21
投票

ValueTask<T> isn't a subset of Task<T>, it's a superset

ValueTask<T>是T和Task<T>的一个有区别的联合,使ReadAsync<T>无需分配同步返回它可用的T值(与使用需要分配Task.FromResult<T>实例的Task<T>相反)。 ValueTask<T>是等待的,因此大多数情况下的消费与Task<T>无法区分。

作为结构体的ValueTask允许编写异步方法,这些异步方法在同步运行时不分配内存而不会影响API一致性。想象一下,有一个Task返回方法的接口。实现此接口的每个类必须返回一个Task,即使它们恰好同步执行(希望使用Task.FromResult)。您当然可以在接口上使用两种不同的方法,一种是同步方法,另一种是异步方法,但这需要2种不同的实现来避免“同步异步”和“异步过同步”。

因此,它允许您编写一个异步或同步的方法,而不是为每个方法编写一个相同的方法。您可以在任何使用Task<T>的地方使用它,但它通常不会添加任何东西。

好吧,它增加了一件事:它向调用者添加了一个隐含的承诺,即该方法实际上使用了ValueTask<T>提供的附加功能。我个人更喜欢选择尽可能多地告诉来电者的参数和返回类型。如果枚举不能提供计数,请不要返回IList<T>;如果可以,不要返回IEnumerable<T>。您的消费者不应该查找任何文档,以了解哪些方法可以合理地同步调用,哪些不可以。

我不认为未来的设计变化是一个有说服力的论点。恰恰相反:如果一个方法改变了它的语义,它应该破坏构建,直到相应地更新对它的所有调用。如果这被认为是不合需要的(并且相信我,我同情不打破构建的愿望),请考虑接口版本控制。

这基本上就是强类型的用途。

如果某些在您的商店中设计异步方法的程序员无法做出明智的决策,那么为每个经验不足的程序员分配一名高级导师并进行每周代码审查可能会有所帮助。如果他们猜错了,请解释为什么应该采用不同的方式。对于资深球员来说,这是一个开销,但它会使得三年级球员的速度提升得更快,而不仅仅是让他们在深层投球并给予他们一些随意的规则。

如果编写该方法的人不知道它是否可以同步调用,那么地球上的人呢?!

如果你有那么多没有经验的程序员编写异步方法,那些同样的人也会调用它们吗?他们是否有资格自己找出哪些可以安全地称为异步,或者他们是否会开始对他们如何称呼这些东西应用类似的任意规则?

这里的问题不是你的返回类型,它是程序员被置于他们尚未准备好的角色。这一定是因为某种原因而发生的,所以我确信修复它不是一件容易的事。描述它当然不是一个解决方案。但是寻找一种方法将问题隐藏在编译器之外也不是解决方案。


11
投票

有一些changes in .Net Core 2.1。从.net core 2.1开始,ValueTask不仅可以表示同步已完成的操作,还可以表示async已完成。此外,我们收到非通用的ValueTask类型。

我将离开与您的问题相关的Stephen Toub comment

我们仍然需要正式化指导,但我希望它对于公共API表面区域将是这样的:

  • 任务提供最大的可用性。
  • ValueTask为性能优化提供了最多选项。
  • 如果您正在编写其他人将覆盖的接口/虚拟方法,则ValueTask是正确的默认选择。
  • 如果您希望API在分配很重要的热路径上使用,那么ValueTask是一个不错的选择。
  • 否则,在性能不重要的情况下,默认为Task,因为它提供了更好的保证和可用性。

从实现的角度来看,许多返回的ValueTask实例仍将由Task支持。

功能不仅可以在.net核心2.1中使用。您将能够将它与System.Threading.Tasks.Extensions包一起使用。

© www.soinside.com 2019 - 2024. All rights reserved.