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


168

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


7
我怀疑这与ValueTask<T>(就分配而言)没有实现实际上异步操作的好处有关(因为在这种情况下ValueTask<T>仍然需要堆分配)。库中还有Task<T>很多其他支持的问题。
乔恩·斯基特

3
@JonSkeet现有的库是一个问题,但这引出了一个问题,从一开始Task应该是ValueTask吗?当将其用于实际的异步内容时,其好处可能不存在,但这是否有害?
Stilgar

8
github.com/dotnet/corefx/issues/4708#issuecomment-160658188,以获得比我无法传达的更多智慧:)
Jon Skeet


3
@JoelMueller剧情变厚了:)
Stilgar

Answers:


245

API文档(添加了重点):

当方法的操作结果很可能同步可用,并且方法的调用频率如此之高,以至于Task<TResult>为每个调用分配新值的成本高昂时,方法可能会返回此值类型的实例。

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

此外,除了通过消费异步操作的结果外,对于其他用途awaitValueTask<TResult>可能会导致更复杂的编程模型,进而可能导致更多分配。例如,考虑一种方法,该方法可以返回Task<TResult>带有缓存任务的常见结果或ValueTask<TResult>。如果结果的使用者希望将其用作a Task<TResult>,例如与in Task.WhenAll和in方法一起使用Task.WhenAny,则ValueTask<TResult>首先需要将the 转换为Task<TResult>using AsTask,这将导致如果使用缓存Task<TResult>就可以避免分配首先。

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


7
@MattThomas:它节省了一个Task分配(如今这是又小又便宜),但是以使调用者的现有分配更大,返回值大小加倍(影响寄存器分配)为代价。对于缓冲读取方案,这是一个明确的选择,但我不建议您默认将其应用于所有接口。
史蒂芬·克雷里

1
是的,Task或者ValueTask可以用作同步返回类型(带有Task.FromResult)。但是,ValueTask如果您有希望同步的东西,那么仍然有价值(heh)。ReadByteAsync是一个经典的例子。我相信 ValueTask它主要是为新的“通道”(低级字节流)创建的,可能还用于性能确实很重要的ASP.NET核心。
史蒂芬·克雷里

1
哦,我知道,大声笑,只是想知道您是否需要在此特定评论中添加一些内容;)
julealgon

2
该PR是否将余额切换为偏爱ValueTask?(ref:blog.marcgravell.com/2019/08/…
斯图尔特

2
@stuartd:现在,我仍然建议使用Task<T>默认值。这仅仅是因为大多数开发人员都不熟悉周围的限制ValueTask<T>(特别是“仅消耗一次”规则和“无阻塞”规则)。也就是说,如果所有你的团队的开发者都用好ValueTask<T>,那么我会建议一个团队层次的喜欢的方针ValueTask<T>
斯蒂芬·克莱里

104

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

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

性能问题应由工程学科解决。制定目标,根据目标衡量您的进度,然后确定如果未达到目标,则如何修改程序,并在整个过程中进行测量,以确保所做的更改实际上是改进。

为什么从一开始就没有使用值类型构建异步/等待。

awaitTask<T>类型已经存在很长时间后,就被添加到C#中。当已经存在一种新类型时,发明它是不恰当的。并await通过大量的设计迭代去解决对被运在2012年完美是好的敌人的一个前; 最好提供一种与现有基础架构良好配合的解决方案,然后,如果用户有需求,请稍后进行改进。

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

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

事情的目的是提高性能。如果无法衡量显着提高性能,它就无法完成任务。无法保证会。


23

ValueTask<T> 不是...的子集 Task<T>它的,而是一个超集

ValueTask<T>是T和a的有区别的并集Task<T>,使其免分配ReadAsync<T>以同步返回它具有的T值(与使用Task.FromResult<T>,它需要分配Task<T>实例)。ValueTask<T>是可以等待的,因此大多数实例的消耗与不会是可区分的Task<T>

ValueTask是一种结构,可以编写异步方法,这些方法在同步运行时不分配内存,而不会损害API的一致性。想象一下,有一个带有Task返回方法的接口。每个实现此接口的类都必须返回一个Task,即使它们碰巧是同步执行(希望使用Task.FromResult)。您当然可以在接口上使用2种不同的方法,一种是同步方法,另一种是异步方法,但这需要2种不同的实现方式,以避免“异步同步”和“异步同步”。

因此,它允许您编写一个异步或同步的方法,而不是为每个方法编写一个其他相同的方法。您可以在任何地方使用它Task<T>但通常不会添加任何东西。

好吧,它增加了一件事:它向调用者增加了一个隐含的承诺,即方法实际上使用了ValueTask<T>提供的其他功能。我个人更喜欢选择尽可能告诉调用者的参数和返回类型。不要回头IList<T>如果枚举不能提供计数,请;IEnumerable<T>如果可以的话不要返回。您的使用者不必查看任何文档即可知道可以合理地同步调用哪些方法,而哪些则不能。

我认为未来的设计变更不会成为令人信服的论点。恰恰相反:如果方法改变了其语义,则应该中断该构建,直到对它的所有调用都进行了相应更新。如果认为这是不希望的(并且相信我,我同情不破坏构建的愿望),请考虑使用接口版本控制。

这本质上就是强类型的目的。

如果您的商店中一些设计异步方法的程序员无法做出明智的决定,那么为每位经验不足的程序员指派一名高级指导者并每周进行一次代码审查可能会有所帮助。如果他们猜错了,请解释为什么应该以不同的方式进行。对于高级人员来说,这是开销,但它可以使初级人员更快地提高速度,而不仅仅是将他们扔进深渊,并给他们一些随便的规则。

如果编写该方法的人不知道是否可以同步调用该方法,那么到底是谁呢?

如果您有那么多没有经验的程序员编写异步方法,那么这些人也都在调用它们吗?他们是否有资格自己找出哪些可以安全地调用异步,还是会开始对它们的调用方式应用类似的任意规则?

这里的问题不是您的返回类型,而是程序员处于他们尚未准备好的角色中。那一定是有原因的,所以我敢肯定修复起来并不容易。描述它当然不是解决方案。但是寻找一种解决问题的方法也无法解决编译器。


4
如果看到返回的方法ValueTask<T>,则认为编写该方法的人是这样做的,因为该方法实际上使用了ValueTask<T>添加的功能。我不明白为什么您认为所有方法都必须具有相同的返回类型。那里的目标是什么?
15ee8f99-57ff-4f92-890c-b56153 '17

2
目标1将是允许改变执行情况。如果我现在不缓存结果,但是以后再添加缓存代码怎么办?目标2是为ValueTask有用时并非所有成员都能理解的团队制定明确的指南。如果使用无害,请始终执行此操作。
史迪加

6
在我看来,这几乎就像让您的所有方法都返回一样,object因为也许有一天您会希望它们返回int而不是string
15ee8f99-57ff-4f92-890c-b56153

3
也许但是,如果您问“为什么要返回字符串而不是对象”这个问题,我可以很容易地回答它,指出类型安全性不足。不在任何地方使用ValueTask的原因似乎更加微妙。
Stilgar

3
@Stilgar我要说的一件事:细微的问题比明显的问题更使您担心。
15ee8f99-57ff-4f92-890c-b56153 '17

20

.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包一起使用。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.