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

Sti*_*gar 146 c# asynchronous

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

Ste*_*ary 218

API文档(重点添加):

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

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

此外,对于除了消耗异步操作的结果之外的用途await,ValueTask<TResult>可以导致更复杂的编程模型,这反过来可以实际上导致更多的分配.例如,考虑一种方法,该方法可以返回Task<TResult>带有缓存任务的a作为常见结果或者ValueTask<TResult>.如果结果的消费者想用它作为Task<TResult>,比如在类似的方法使用Task.WhenAllTask.WhenAny,则ValueTask<TResult>首先需要转换成Task<TResult>使用AsTask,这导致如果高速缓存会被避免了分配Task<TResult>已经使用首先.

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

  • @MattThomas:它保存了一个`Task`分配(这些天很小而且便宜),但代价是使*调用者的*现有分配更大,并使返回值的大小加倍(影响寄存器分配).虽然它是缓冲读取方案的明确选择,但默认情况下将其应用于所有接口并不是我推荐的. (5认同)
  • @stuartd:目前,我仍然建议使用 `Task&lt;T&gt;` 作为默认值。这只是因为大多数开发人员不熟悉 ValueTask&lt;T&gt; 周围的限制(具体来说,“仅消耗一次”规则和“无阻塞”规则)。也就是说,如果您团队中的所有开发人员都擅长“ValueTask&lt;T&gt;”,那么我会推荐一个“团队级别”的优先指南“ValueTask&lt;T&gt;”。 (3认同)
  • 是的,`Task` 或`ValueTask` 都可以用作同步返回类型(使用`Task.FromResult`)。但是如果你有一些你*期望*同步的东西,那么`ValueTask`中仍然有价值(呵呵)。`ReadByteAsync` 是一个经典的例子。我*相信* `ValueTask` 主要是为新的“通道”(低级字节流)创建的,可能也用于性能真正重要的 ASP.NET 核心。 (2认同)
  • [此PR](https://github.com/dotnet/coreclr/pull/26310)是否将余额切换为偏好ValueTask?(参考:https://blog.marcgravell.com/2019/08/prefer-valuetask-to-task-always-and.html) (2认同)

Eri*_*ert 96

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

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

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

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

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

我还注意到,允许用户提供的类型作为编译器生成方法的输出的新功能增加了相当大的风险和测试负担.当您可以返回的唯一内容无效或任务时,测试团队不必考虑返回某些绝对疯狂类型的任何情况.测试编译器装置,找出不只是什么节目的人很可能会写,但什么程序是可以写的,因为我们希望编译器来编译所有的法律的程序,不可能每个人都理智的程序.这太贵了.

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

事物的目的是提高性能.它不会做的工作,如果它不可测量显著提高性能.它无法保证.

  • `当可以注册引用时,大于 64 位的结构可能不会被注册`...如果其他人想知道,这里的“注册”一词可能指的是“存储在 CPU 寄存器中”(这是最快的可用内存位置)。 (8认同)
  • 我取消链接此回复只是为了再次喜欢它 (3认同)

15e*_*153 23

ValueTask<T>它不是一个子集Task<T>,它是一个超集.

ValueTask<T>是T和a的区别联合Task<T>,使其无分配ReadAsync<T>以同步返回它可用的T值(与使用相比Task.FromResult<T>,需要分配一个Task<T>实例).ValueTask<T>是等待的,所以大多数实例的消费都与a无法区分Task<T>.

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

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

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

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

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

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

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

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

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

  • 在我看来,这几乎就像让你的所有方法都返回`object`,因为也许有一天你会希望它们返回`int`而不是`string`. (5认同)
  • 如果我看到一个返回`ValueTask <T>`的方法,我认为编写该方法的人就是这样做的,因为该方法实际上使用了`ValueTask <T>`添加的功能.我不明白为什么你认为你的所有方法都有相同的返回类型.那里的目标是什么? (4认同)
  • 也许但是如果你问一个问题"为什么你会返回一个字符串而不是一个对象"我可以很容易地回答它指出缺乏类型安全性.不使用ValueTask的原因似乎更加微妙. (3认同)
  • @Stilgar我会说的一件事:微妙的问题应该让你担心的不仅仅是明显的问题. (3认同)
  • 目标1将是允许改变执行情况。如果我现在不缓存结果,但是以后再添加缓存代码怎么办?目标2是为ValueTask有用时并非所有成员都能理解的团队制定明确的指南。如果使用无害,请始终执行此操作。 (2认同)
  • 是的,他们确实让我担心,这就是我试图理解这里具体细微问题的原因.传达意图是一个好点,但我怀疑它是唯一的一个. (2认同)

uns*_*Ptr 16

.Net Core 2.1有一些变化.从.net core 2.1开始,ValueTask不仅可以表示同步已完成的操作,还可以表示async已完成.另外我们收到非泛型ValueTask类型.

我将留下与您的问题相关的Stephen Toub 评论:

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

  • 任务提供最大的可用性.

  • ValueTask为性能优化提供了最多选项.

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

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

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

  • 今天更多来自斯蒂芬:https://blogs.msdn.microsoft.com/dotnet/2018/11/07/understanding-the-whys-whats-and-whens-of-valuetask/ (2认同)

Shu*_*han 11

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

当某件事通常或总是真正异步时(即不立即完成),请使用任务;当某些事情通常或总是同步时使用 ValueTask,即值将是内联已知的;还可以在无法知道答案的多态场景(虚拟、接口)中使用 ValueTask。

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

当我有类似的问题时,我按照上面的博客文章进行了最近的项目。

截至 2019 年 8 月 23 日,来自Marc Gravell 的更新#2 (来自他的博客):

因此,回到之前何时使用 Task 与 ValueTask 的问题,IMO 的答案现在很明显:

使用 ValueTask[],除非你绝对不能,因为现有的 API 是 Task[],即使这样:至少考虑一下 API 中断。

另请记住:仅等待任何单个可等待表达式一次

如果我们将这两件事放在一起,图书馆和 BCL 就可以在后台自由地创造奇迹,以提高性能,而无需调用者关心。

  • 该博文中的建议已更新:使用 ValueTask[&lt;T&gt;],除非您绝对不能,因为现有 API 是 Task[&lt;T&gt;],即便如此:至少考虑 API 中断 (9认同)