为什么要用Task<T>在ValueTask< T>在c#中?

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

71078 次浏览

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

方法可能会返回此值类型的实例,如果方法的操作结果可能会同步可用,而该方法预计会被频繁调用,以至于为每次调用分配一个新的Task<TResult>的代价将是令人望而却步的。

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

此外,对于通过await消耗异步操作结果以外的其他用途,ValueTask<TResult>可能导致更复杂的编程模型,这实际上会导致更多的分配。例如,考虑一个方法,它可以返回带有缓存任务的Task<TResult>作为公共结果,也可以返回ValueTask<TResult>。如果结果的使用者希望将其用作Task<TResult>,例如使用with in方法,如Task.WhenAllTask.WhenAny,则ValueTask<TResult>首先需要使用AsTask将其转换为Task<TResult>,这将导致如果首先使用缓存的Task<TResult>,则可以避免分配。

因此,任何异步方法的默认选择应该是返回__ABC0或Task<TResult>。只有当性能分析证明它值得使用时,才应该使用__ABC2而不是Task<TResult>

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

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

性能问题应该用工程学科来解决。制定目标,根据目标衡量你的进展,然后决定如何在目标没有达到的情况下修改程序,在此过程中进行衡量,以确保你的更改实际上是改进。

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

awaitTask<T>类型已经存在很久之后才被添加到c#中。当一种新类型已经存在的时候,再去发明它有点反常。await在2012年发布之前经历了大量的设计迭代。完美是美好的敌人;更好的方法是发布一个与现有基础设施良好合作的解决方案,然后如果有用户需求,以后再提供改进。

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

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

这个东西的目的是提高性能。如果可以测定的程度显著不能提高性能,它就不能完成工作。没有人能保证它会成功。

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

我将留下Stephen Toub 评论,这与你的问题有关:

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

  • Task提供了最大的可用性。

  • 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就可以在后台自由地创造奇迹,提高性能,而不需要调用者关心。