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

Posted

技术标签:

【中文标题】为啥要在 C# 中使用 Task<T> 而不是 ValueTask<T>?【英文标题】:Why would one use Task<T> over ValueTask<T> in C#?为什么要在 C# 中使用 Task<T> 而不是 ValueTask<T>? 【发布时间】:2017-08-17 10:51:00 【问题描述】:

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

【问题讨论】:

我怀疑这与ValueTask&lt;T&gt; 的好处(在分配方面)没有实现实际上异步操作(因为在那种情况下ValueTask&lt;T&gt; 将仍然需要堆分配)。还有Task&lt;T&gt; 在库中有很多其他支持的问题。 @JonSkeet 现有库是个问题,但这引出了一个问题,Task 从一开始就应该是 ValueTask 吗?将它用于实际的异步内容时可能不存在好处,但它有害吗? 请参阅github.com/dotnet/corefx/issues/4708#issuecomment-160658188,了解我无法传达的更多智慧:) @JoelMueller 剧情变厚了 :) 当 Jon Skeet、两个 Stephens(Cleary 和 Toub)和 Eric Lippert 都有宝贵的贡献时,你知道这是一个重要的问题...... 【参考方案1】:

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

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

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

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

【讨论】:

那篇博文的建议已经更新:使用 ValueTask[],除非你绝对不能,因为现有的 API 是 Task[],即使那样:至少:考虑 API 中断【参考方案2】:

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

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

我们仍然需要正式的指导,但我希望这会有所作为 像这样的公共 API 表面积:

Task 提供了最大的可用性。

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

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

如果您希望 API 用于分配很重要的热路径,ValueTask 是一个不错的选择。

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

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

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

【讨论】:

斯蒂芬今天提供的更多信息:blogs.msdn.microsoft.com/dotnet/2018/11/07/…【参考方案3】:

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

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

性能问题应该通过工程学科来解决。制定目标,根据目标衡量您的进度,然后决定在未达到目标时如何修改程序,并在此过程中进行衡量以确保您的更改实际上是改进。

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

await 是在 Task&lt;T&gt; 类型已经存在很久之后才添加到 C# 中的。在已经存在一种新类型的情况下发明一种新类型会有些不合常理。 await 在确定 2012 年发布的那款之前,经历了无数次设计迭代。完美是美好的敌人;最好发布一个与现有基础架构配合良好的解决方案,然后如果有用户需求,稍后再提供改进。

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

有人能解释一下 ValueTask 什么时候无法完成这项工作吗?

这个东西的目的是提高性能。如果它不能显着显着提高性能,它就无法完成这项工作。不能保证一定会。

【讨论】:

Structs that are larger than 64 bits might not be enregistered when a reference could be enregistered...如果其他人想知道,这里的“注册”一词可能是指“存储在 CPU 寄存器中”(这是可用的最快内存位置)。 我取消链接此回复只是为了让我再次喜欢它【参考方案4】:

来自the API docs(已添加重点):

方法可能会返回此值类型的实例,当它们的操作结果可能会同步可用时并且当方法被期望如此频繁地调用以致分配新的成本时Task&lt;TResult&gt; 每次通话都会被禁止。

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

此外,对于通过await 使用异步操作的结果以外的用途,ValueTask&lt;TResult&gt; 可能会导致更复杂的编程模型,这实际上会导致更多的分配。例如,考虑一个方法,它可以返回带有缓存任务的Task&lt;TResult&gt; 作为公共结果或ValueTask&lt;TResult&gt;。如果结果的使用者想要将其用作Task&lt;TResult&gt;,例如在Task.WhenAllTask.WhenAny 等方法中使用,则首先需要使用@987654340 将ValueTask&lt;TResult&gt; 转换为Task&lt;TResult&gt; @,如果一开始就使用了缓存的Task&lt;TResult&gt;,这将导致本可以避免的分配。

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

【讨论】:

@MattThomas:它节省了单个 Task 分配(如今它既小又便宜),但代价是使 调用者 的现有分配更大,并使返回值的大小(影响寄存器分配)。虽然它是缓冲读取场景的明确选择,但我不建议将其默认应用于所有接口。 对,TaskValueTask 都可以用作同步返回类型(与Task.FromResult)。但是如果你有一些你期望同步的东西,ValueTask 仍然有价值(呵呵)。 ReadByteAsync 是一个经典的例子。我相信 ValueTask 主要是为新的“通道”(低级字节流)创建的,可能也用于性能真正重要的 ASP.NET 核心。 哦,我知道,大声笑,只是想知道您是否有什么要添加到该特定评论的内容;) this PR 是否将平衡切换到更喜欢 ValueTask? (参考:blog.marcgravell.com/2019/08/…) @stuartd:目前,我仍然建议使用Task&lt;T&gt; 作为默认值。这只是因为大多数开发人员不熟悉ValueTask&lt;T&gt; 周围的限制(特别是“仅消费一次”规则和“不阻塞”规则)。也就是说,如果您团队中的所有开发人员都对ValueTask&lt;T&gt; 感到满意,那么我会推荐一个团队级别 的指导方针,即首选ValueTask&lt;T&gt;

以上是关于为啥要在 C# 中使用 Task<T> 而不是 ValueTask<T>?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Task<T> 不是协变的?

为啥不调用 Task<T>.Result 死锁?

C# Async与Await的使用

如何使用反射动态创建通用 C# 对象? [复制]

C# 中 Task.FromResult<TResult> 的用途是啥

为啥 C# 数组对 Enumeration 使用引用类型,而 List<T> 使用可变结构?