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

从C#7.0开始,asynchronous方法可以返回ValueTask <T>。 解释说,当我们有一个caching的结果或者通过同步代码模拟asynchronous的时候应该使用它。 然而,我仍然不明白什么是总是使用ValueTask的问题,或者事实上,为什么asynchronous/等待不是从一开始就build立了一个值types。 ValueTask何时无法完成这项工作?

从API文档 (强调添加):

方法可能会返回一个这个值types的实例,当它们的操作结果可能会同步可用时, 以及预计这个方法被频繁调用时,为每个调用分配一个新的Task<TResult>的成本将会过高。

使用ValueTask<TResult>代替Task<TResult>是有折衷的。 例如,虽然ValueTask<TResult>可以帮助在成功结果同步可用的情况下避免分配,但它也包含两个字段,而Task<TResult>作为引用types是单个字段。 这意味着一个方法调用结束了返回两个字段值的数据而不是一个,这是更多的数据复制。 这也意味着,如果在async方法中等待返回其中一个的方法,那么由于需要存储两个字段而不是单个引用的结构,该async方法的状态机将会更大。

此外,除了通过await消耗asynchronous操作的结果以外, ValueTask<TResult>可能会导致更复杂的编程模型,这反过来又会导致更多的分配。 例如,考虑一种方法,可以将具有caching任务的Task<TResult>作为通用结果返回,也可以返回ValueTask<TResult> 。 如果结果的使用者想要将它用作Task<TResult> ,例如在Task.WhenAllTask.WhenAny等方法中使用,则首先需要将ValueTask<TResult>转换为Task<TResult>使用AsTask ,如果首先使用了caching的Task<TResult> ,则导致分配将被避免。

因此, 任何asynchronous方法的默认select应该是返回一个TaskTask<TResult> 只有性能分析certificate值得使用ValueTask<TResult>而不是Task<TResult>

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

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

性能问题应该以工程学科来解决。 制定目标,根据目标衡量你的进度,然后决定如果目标没有得到满足,如何修改程序,一路测量,以确保你的变化实际上是改善。

为什么asynchronous/等待不是从一开始就用值types构build的。

Task<T>types已经存在之后, await已经被添加到C#了。 如果已经存在的话,创造一种新的types会有些不正当。 在await 2012年发货之前, await经过了大量的devise迭代。完美是善的敌人; 更好地发布一个适合现有基础架构的解决scheme,然后如果有用户需求,稍后再提供改进。

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

有人可以解释什么时候ValueTask将无法完成这项工作?

事情的目的是提高性能。 如果不能 提高绩效,它就不会做这项工作。 这是不能保证的。

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

ValueTask<T>是T和Task<T>一个区分的联合,使它无需为ReadAsync<T>分配,以同步返回它可用的T值(与使用Task.FromResult<T>需要分配一个Task<T>实例)。 ValueTask<T>是可以等待的,所以实例的大部分消耗将与Task<T>无法区分。

ValueTask作为一个结构体,可以编写asynchronous方法,在不影响API一致性的情况下同步运行时不分配内存。 想象一下有一个任务返回方法的接口。 实现此接口的每个类都必须返回一个Task,即使它们碰巧同步执行(希望使用Task.FromResult)。 你当然可以在接口上有两种不同的方法,一种是同步方式,另一种是asynchronous方式,但是这需要2种不同的实现来避免“通过asynchronous同步”和“asynchronous同步”。

所以它可以让你编写一个asynchronous或同步的方法,而不是为每个方法编写一个相同的方法。 你可以在任何使用Task<T>地方使用它,但是它往往不会添加任何东西。

那么,它增加了一件事情:它向调用者添加了一个隐含的承诺,即该方法实际上使用了ValueTask<T>提供的附加function。 我个人更喜欢select尽可能告诉调用者的参数和返回types。 如果枚举不能提供计数,则不要返回IList<T> ; 如果可以,不要返回IEnumerable<T> 。 您的消费者不应该查找任何文档,以了解您的哪些方法可以合理地同步调用,哪些不能。

我不认为未来的devise变化是一个令人信服的论据。 恰恰相反:如果一个方法改变它的语义,它应该打破构build,直到所有的调用都相应地更新。 如果这被认为是不可取的(并相信我,我同情一个不打破构build的愿望),考虑接口版本控制。

这基本上是打字的重点。

如果某些在您的商店中deviseasynchronous方法的程序员无法做出明智的决定,那么为每位经验较less的程序员指派一名高级指导并进行每周代码审查可能会有帮助。 如果他们猜错了,请解释为什么应该以不同的方式来完成。 这对高年级学生来说是个开销,但是这会让后辈的速度更快,而不仅仅是把他们扔在深处,给他们一些随意的规则。

如果写这个方法的人不知道是否可以同步调用,那么地球上的人呢?!

如果你有那么多缺乏经验的程序员编写asynchronous方法,那么同样的人也会这样称呼它们吗? 他们是否有资格自行找出哪些是可以安全调用asynchronous的,或者他们是否会开始对类似的任意规则应用这些东西?

这里的问题不是你的返回types,而是程序员被放在他们还没准备好的angular色中。 这肯定是有原因的,所以我相信解决这个问题不是一件容易的事情。 描述它当然不是一个解决scheme。 但是, 寻找一种方法来通过编译器偷偷摸摸的问题也不是一个解决scheme。