来自API文档(强调添加):
当方法的操作结果可能会同步可用且方法的调用频率高到每次分配一个新的
Task<TResult>
都是禁止的时,该方法可能返回这个值类型的实例。使用
此外,除了通过ValueTask<TResult>
代替Task<TResult>
存在权衡。例如,虽然在成功结果同步可用的情况下,ValueTask<TResult>
可以帮助避免分配,但它包含两个字段,而Task<TResult>
作为引用类型只有一个字段。这意味着方法调用将返回两个字段的数据而不是一个,这是更多需要复制的数据。它还意味着,如果一个返回其中之一的方法在一个async
方法内被等待,那么由于需要存储两个字段的结构体,该async
方法的状态机将变得更大。await
消耗异步操作的结果之外,ValueTask<TResult>
可能会导致更加复杂的编程模型,反过来实际上会导致更多的分配。例如,考虑一个可以返回具有缓存任务作为公共结果的Task<TResult>
或ValueTask<TResult>
的方法。如果结果的消费者想将其用作Task<TResult>
,例如在Task.WhenAll
和Task.WhenAny
这样的方法中使用,则需要使用AsTask
将ValueTask<TResult>
转换为Task<TResult>
,这会导致一次分配,如果一开始使用了缓存的Task<TResult>
,则可以避免这种情况。因此,任何异步方法的默认选择应该是返回
Task
或Task<TResult>
。只有在性能分析证明值得时,才应该使用ValueTask<TResult>
代替Task<TResult>
。
Task
,尽管这个分配现在已经很小和便宜了。但代价是增加了调用者的现有分配量,并且将返回值的大小翻倍(影响寄存器分配)。虽然对于缓冲读取场景来说这是一个明确的选择,但我不建议将其默认应用于所有接口。 - Stephen ClearyTask
或ValueTask
用作同步返回类型(使用Task.FromResult
)。但是,如果您有某些预期为同步的内容,则仍然有价值(嘿)使用ValueTask
。ReadByteAsync
是一个经典例子。我相信 ValueTask
主要是为了新的“通道”(低级字节流)而创建的,可能也在性能真正重要的 ASP.NET Core 中使用。 - Stephen ClearyValueTask
仅适用于许多同步等待的情况,其中 Task
对象的分配开销是不可取的(根据性能分析测量)。 - Stephen ClearyTask<T>
作为默认选项。这是因为大多数开发人员不熟悉ValueTask<T>
的限制(特别是“只能消费一次”规则和“禁止阻塞”规则)。但是,如果您团队中的所有开发人员都熟悉ValueTask<T>
,那么我建议采用团队级别的指导方针,优先选择ValueTask<T>
。 - Stephen Cleary然而,我仍然不明白总是使用ValueTask有什么问题
结构体类型并非免费。复制大于引用大小的结构体可能比复制引用更慢。存储大于引用大小的结构体需要比存储引用更多的内存。大于64位的结构体可能无法在可以注册引用的情况下进行寄存器分配。较低的收集压力所带来的好处可能不会超过成本。
性能问题应该采用工程学方法。设定目标,测量进展与目标之间的差距,然后根据需要修改程序,在修正期间进行测量,以确保更改实际上是改进。
为什么async/await没有一开始就使用值类型。
await
在 C# 中添加时,Task<T>
类型已经存在了很久。发明新类型就显得有些不合适。此外,await
经历了许多设计迭代,最终在2012年发布了一个解决方案。完美是敌人,有改进需求时,最好先使用现有的基础设施提供一个可行的解决方案,然后再提供改进。
我还注意到,允许用户提供的类型成为编译器生成方法的输出的新功能会增加风险和测试负担。当你只能返回void或task时,测试团队不必考虑任何情况,其中会返回一些绝对疯狂的类型。测试编译器意味着要找出人们可能编写的所有程序,而不仅仅是所有明智的程序,因为我们希望编译器编译所有合法的程序,而不仅仅是所有合理的程序。这是很昂贵的。
有人能解释一下ValueTask不能胜任任务的情况吗?
这个东西的目的是提高性能。如果它没有明显且显著地提高性能,则它就没有完成任务。它并不保证一定会提高性能。
ValueTask
类型。我们仍然需要规范指导,但我期望对于公共 API 表面积,它会像这样:
任务提供最多的可用性。
ValueTask 提供了最多的性能优化选项。
如果您正在编写其他人将覆盖的接口/虚方法,则 ValueTask 是正确的默认选择。
如果您希望在热路径上使用 API,其中分配很重要,则 ValueTask 是一个很好的选择。
否则,在性能不关键的情况下,默认为 Task,因为它提供了更好的保证和可用性。
从实现的角度来看,许多返回的 ValueTask 实例仍将由 Task 支持。
该功能不仅可以在 .net core 2.1 中使用,而且您还可以使用 System.Threading.Tasks.Extensions 包。马克(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可以在后台进行奇迹般的性能改进,而不需要调用者担心。
ValueTask<T>
的好处(在分配方面)不会在实际异步操作中显现有关(因为在这种情况下,ValueTask<T>
仍需要堆分配)。此外,Task<T>
在库中还具有许多其他支持相关的问题。 - Jon Skeet