为什么在C#中使用Task <t>而不是ValueTask <t>?

时间:2017-03-24 13:16:57

标签: c# asynchronous

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

5 个答案:

答案 0 :(得分:185)

来自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>

答案 1 :(得分:83)

  

但是我仍然不明白使用ValueTask始终存在什么问题

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

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

  

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

await类型已经存在之后,

Task<T>被添加到C#中。如果已经存在新类型,那么发明新类型会有些不正常。并且await经历了大量的设计迭代,然后才决定2012年发布的那个。完美是善的敌人;最好是提供一个与现有基础设施配合良好的解决方案,然后如果有用户需求,请在以后提供改进。

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

  

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

事物的目的是提高性能。如果可测量并且显着提高性能,它就无法完成工作。无法保证它会。

答案 2 :(得分:21)

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

  
    

ValueTask<T>是T与Task<T>的区别联合,使ReadAsync<T>无分配,可同步返回其可用的T值(与使用{{相比) 1}},需要分配一个Task.FromResult<T>实例)。 Task<T>是等待的,因此大多数实例的消费与ValueTask<T>无法区分。

  
     

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

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

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

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

这实际上是强类型的用途。

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

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

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

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

答案 3 :(得分: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 包一起使用。

答案 4 :(得分:1)

Marc的最新信息(2019年8月)

在某些事物通常或总是真正异步的,即不是立即完成时使用Task;当某些东西通常或总是要同步时使用ValueTask,即该值将内联已知;还可以在您不知道答案的多态场景(虚拟,界面)中使用ValueTask。

来源:https://blog.marcgravell.com/2019/08/prefer-valuetask-to-task-always-and.html

当我有类似问题时,我在上面的博客文章中关注了一个最近的项目。