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

289
从C# 7.0版本开始,异步方法可以返回ValueTask<T>。解释称,当我们有缓存结果或者通过同步代码模拟异步时应该使用它。但是我仍然不理解为什么不能一直使用ValueTask,或者说为什么async/await一开始就没有使用值类型。在什么情况下ValueTask无法胜任?

13
我猜测这与ValueTask<T>的好处(在分配方面)不会在实际异步操作中显现有关(因为在这种情况下,ValueTask<T>仍需要堆分配)。此外,Task<T>在库中还具有许多其他支持相关的问题。 - Jon Skeet
4
@JonSkeet 存在的库是一个问题,但这引发了一个问题:Task从一开始就应该是ValueTask吗?当用于实际的异步操作时,可能并不存在优势,但它是否有害? - Stilgar
9
请参考以下链接获取更多智慧,比我能传达的更详细:https://github.com/dotnet/corefx/issues/4708#issuecomment-160658188 :) - Jon Skeet
3
@JoelMueller 情节越来越扑朔迷离 :) - Stilgar
10
当Jon Skeet、两位Stephens(Cleary和Toub)以及Eric Lippert都做出了有价值的贡献时,你就知道这是一个重要的问题。 - solublefish
显示剩余5条评论
4个回答

354

来自API文档(强调添加):

当方法的操作结果可能会同步可用且方法的调用频率高到每次分配一个新的Task<TResult>都是禁止的时,该方法可能返回这个值类型的实例。

使用ValueTask<TResult>代替Task<TResult>存在权衡。例如,虽然在成功结果同步可用的情况下,ValueTask<TResult>可以帮助避免分配,但它包含两个字段,而Task<TResult>作为引用类型只有一个字段。这意味着方法调用将返回两个字段的数据而不是一个,这是更多需要复制的数据。它还意味着,如果一个返回其中之一的方法在一个async方法内被等待,那么由于需要存储两个字段的结构体,该async方法的状态机将变得更大。

此外,除了通过await消耗异步操作的结果之外,ValueTask<TResult>可能会导致更加复杂的编程模型,反过来实际上会导致更多的分配。例如,考虑一个可以返回具有缓存任务作为公共结果的Task<TResult>ValueTask<TResult>的方法。如果结果的消费者想将其用作Task<TResult>,例如在Task.WhenAllTask.WhenAny这样的方法中使用,则需要使用AsTaskValueTask<TResult>转换为Task<TResult>,这会导致一次分配,如果一开始使用了缓存的Task<TResult>,则可以避免这种情况。

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


13
@MattThomas说:这种做法可以避免多分配一个Task,尽管这个分配现在已经很小和便宜了。但代价是增加了调用者的现有分配量,并且将返回值的大小翻倍(影响寄存器分配)。虽然对于缓冲读取场景来说这是一个明确的选择,但我不建议将其默认应用于所有接口。 - Stephen Cleary
4
正确,可以将TaskValueTask用作同步返回类型(使用Task.FromResult)。但是,如果您有某些预期为同步的内容,则仍然有价值(嘿)使用ValueTaskReadByteAsync是一个经典例子。我相信 ValueTask 主要是为了新的“通道”(低级字节流)而创建的,可能也在性能真正重要的 ASP.NET Core 中使用。 - Stephen Cleary
1
我会说,ValueTask 仅适用于许多同步等待的情况,其中 Task 对象的分配开销是不可取的(根据性能分析测量)。 - Stephen Cleary
5
这个PR是否把倾向于使用ValueTask的权重提高了?(参考:https://blog.marcgravell.com/2019/08/prefer-valuetask-to-task-always-and.html) - stuartd
6
目前,我仍然建议将Task<T>作为默认选项。这是因为大多数开发人员不熟悉ValueTask<T>的限制(特别是“只能消费一次”规则和“禁止阻塞”规则)。但是,如果您团队中的所有开发人员都熟悉ValueTask<T>,那么我建议采用团队级别的指导方针,优先选择ValueTask<T> - Stephen Cleary
显示剩余9条评论

153

然而,我仍然不明白总是使用ValueTask有什么问题

结构体类型并非免费。复制大于引用大小的结构体可能比复制引用更慢。存储大于引用大小的结构体需要比存储引用更多的内存。大于64位的结构体可能无法在可以注册引用的情况下进行寄存器分配。较低的收集压力所带来的好处可能不会超过成本。

性能问题应该采用工程学方法。设定目标,测量进展与目标之间的差距,然后根据需要修改程序,在修正期间进行测量,以确保更改实际上是改进。

为什么async/await没有一开始就使用值类型。

await 在 C# 中添加时,Task<T> 类型已经存在了很久。发明新类型就显得有些不合适。此外,await 经历了许多设计迭代,最终在2012年发布了一个解决方案。完美是敌人,有改进需求时,最好先使用现有的基础设施提供一个可行的解决方案,然后再提供改进。

我还注意到,允许用户提供的类型成为编译器生成方法的输出的新功能会增加风险和测试负担。当你只能返回void或task时,测试团队不必考虑任何情况,其中会返回一些绝对疯狂的类型。测试编译器意味着要找出人们可能编写的所有程序,而不仅仅是所有明智的程序,因为我们希望编译器编译所有合法的程序,而不仅仅是所有合理的程序。这是很昂贵的。

有人能解释一下ValueTask不能胜任任务的情况吗?

这个东西的目的是提高性能。如果它没有明显显著地提高性能,则它就没有完成任务。它并不保证一定会提高性能。


8
如果结构体大小超过64位,当引用可以被存储在CPU寄存器中时,该结构体可能无法被存储在寄存器中。如果有人想知道,这里的“enregistered”一词可能是指“被存储在CPU寄存器中”的意思(这是可用的最快速度的内存位置)。 - Eric Mutta
3
我取消了与此回复的链接,只是为了能够再次点赞它。 - Leonardo

35
在 .Net Core 2.1 中有一些 变化。从 .net core 2.1 开始,ValueTask 可以表示不仅是同步完成的操作,还包括异步完成的操作。此外,我们还收到了非泛型的 ValueTask 类型。
我将留下 Stephen Toub 的 评论,这与您的问题有关:
引用:

我们仍然需要规范指导,但我期望对于公共 API 表面积,它会像这样:

  • 任务提供最多的可用性。

  • ValueTask 提供了最多的性能优化选项。

  • 如果您正在编写其他人将覆盖的接口/虚方法,则 ValueTask 是正确的默认选择。

  • 如果您希望在热路径上使用 API,其中分配很重要,则 ValueTask 是一个很好的选择。

  • 否则,在性能不关键的情况下,默认为 Task,因为它提供了更好的保证和可用性。

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

该功能不仅可以在 .net core 2.1 中使用,而且您还可以使用 System.Threading.Tasks.Extensions 包。

3
今天更多来自Stephen:https://blogs.msdn.microsoft.com/dotnet/2018/11/07/understanding-the-whys-whats-and-whens-of-valuetask/ - Mike Cheel

9

马克(Marc)最新消息(2019年8月):

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

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

我在最近的项目中遵循了上述博客文章,当我有类似问题时。

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

因此,回到早期关于何时使用Task vs ValueTask的问题,我的答案现在显而易见:

使用ValueTask[],除非您绝对不能使用它,因为现有API是Task[],即使如此:至少考虑API中断。

同时请注意:仅一次等待任何单个可等待表达式

如果我们将这两件事结合起来,库和BCL可以在后台进行奇迹般的性能改进,而不需要调用者担心。


9
该博客文章上的建议已更新:除非现有 API 是 Task[<T>],否则使用 ValueTask[<T>],除非您绝对不行。即使这样,至少也要考虑更改 API。 - extremeandy
"只有等待单个可等待表达式一次" 只适用于 ValueTask,就我所读。所以,如果您需要多次等待,那似乎是使用 Task 的一个理由。 - Jared Thirsk

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