ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

P
Pierre Arnaud

the API docs(强调添加):

方法可能会返回此值类型的实例,因为它们的操作结果可能会同步可用,并且当方法被期望如此频繁地调用以致于为每次调用分配新的 Task 的成本将过高时.使用 ValueTask 而不是 Task 需要权衡取舍。例如,虽然 ValueTask 可以帮助避免在成功结果同步可用的情况下进行分配,但它也包含两个字段,而作为引用类型的 Task 是单个字段。这意味着方法调用最终会返回两个值得数据的字段,而不是一个需要复制的数据。这也意味着,如果在异步方法中等待返回其中之一的方法,则该异步方法的状态机将更大,因为需要存储两个字段而不是单个引用的结构。此外,对于通过 await 使用异步操作的结果以外的用途,ValueTask 可能会导致更复杂的编程模型,这实际上会导致更多的分配。例如,考虑一个方法,它可以返回一个带有缓存任务的 Task 作为公共结果,也可以返回一个 ValueTask。如果结果的使用者想要将其用作 Task,例如在 Task.WhenAll 和 Task.WhenAny 等方法中使用 with,则首先需要将 ValueTask 转换为 Task使用 AsTask,如果一开始就使用了缓存的 Task,这将导致本可以避免的分配。因此,任何异步方法的默认选择应该是返回 Task 或 Task。只有当性能分析证明值得使用 ValueTask 而不是 Task 时。


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

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

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

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

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

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

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

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

事情的目的是提高性能。如果它不能显着地提高性能,它就无法完成这项工作。不能保证它会。


Structs that are larger than 64 bits might not be enregistered when a reference could be enregistered...如果其他人想知道,这里的“注册”一词可能是指“存储在 CPU 寄存器中”(这是可用的最快内存位置)。
我取消了这个回复的链接,所以我可以再次喜欢它
C
Community

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

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

我们仍然需要正式的指导,但我希望公共 API 表面区域会是这样的:任务提供了最大的可用性。 ValueTask 提供了最多的性能优化选项。如果您正在编写其他人将覆盖的接口/虚拟方法,那么 ValueTask 是正确的默认选择。如果您希望 API 用于分配很重要的热路径,那么 ValueTask 是一个不错的选择。否则,在性能不重要的情况下,默认为 Task,因为它提供了更好的保证和可用性。从实现的角度来看,许多返回的 ValueTask 实例仍将由 Task 支持。

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


斯蒂芬今天提供的更多信息:blogs.msdn.microsoft.com/dotnet/2018/11/07/…
S
Shubhan

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

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

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

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


该博客文章的建议已更新:使用 ValueTask[],除非您绝对不能,因为现有 API 是 Task[],即使这样:至少考虑 API 中断