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


从API文档(强调添加):

Methods may return an instance of this value type when it's likely that the result of their operations will be available synchronously and when the method is expected to be invoked so frequently that the cost of allocating a new Task<TResult> for each call will be prohibitive. There are tradeoffs to using a ValueTask<TResult> instead of a Task<TResult>. For example, while a ValueTask<TResult> can help avoid an allocation in the case where the successful result is available synchronously, it also contains two fields whereas a Task<TResult> as a reference type is a single field. This means that a method call ends up returning two fields worth of data instead of one, which is more data to copy. It also means that if a method that returns one of these is awaited within an async method, the state machine for that async method will be larger due to needing to store the struct that's two fields instead of a single reference. Further, for uses other than consuming the result of an asynchronous operation via await, ValueTask<TResult> can lead to a more convoluted programming model, which can in turn actually lead to more allocations. For example, consider a method that could return either a Task<TResult> with a cached task as a common result or a ValueTask<TResult>. If the consumer of the result wants to use it as a Task<TResult>, such as to use with in methods like Task.WhenAll and Task.WhenAny, the ValueTask<TResult> would first need to be converted into a Task<TResult> using AsTask, which leads to an allocation that would have been avoided if a cached Task<TResult> had been used in the first place. As such, the default choice for any asynchronous method should be to return a Task or Task<TResult>. Only if performance analysis proves it worthwhile should a ValueTask<TResult> be used instead of Task<TResult>.


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

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

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

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

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

I note also that the new feature of allowing user-supplied types to be the output of a compiler-generated method adds considerable risk and testing burden. When the only things you can return are void or a task, the testing team does not have to consider any scenario in which some absolutely crazy type is returned. Testing a compiler means figuring out not just what programs people are likely to write, but what programs are possible to write, because we want the compiler to compile all legal programs, not just all sensible programs. That's expensive.

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

这个东西的目的是提高性能。如果它不能显著地提高性能,它就不能发挥作用。没有人能保证它会成功。


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

我将留给Stephen Toub关于你的问题的评论:

We still need to formalize guidance, but I expect it'll be something like this for public API surface area: Task provides the most usability. ValueTask provides the most options for performance optimization. If you're writing an interface / virtual method that others will override, ValueTask is the right default choice. If you expect the API to be used on hot paths where allocations will matter, ValueTask is a good choice. Otherwise, where performance isn't critical, default to Task, as it provides better guarantees and usability. From an implementation perspective, many of the returned ValueTask instances will still be backed by 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

在最近的一个项目中,我也遇到了类似的问题。

截至2019年8月23日Marc Gravell的更新#2(来自他的博客):

所以,回到之前的问题,什么时候使用Task vs ValueTask,在我看来,答案现在是显而易见的: 使用ValueTask[],除非你绝对不能,因为现有的API是Task[],即使这样:至少考虑API中断。

还要记住:只等待任何一个可等待的表达式一次

如果我们把这两件事放在一起,库和BCL就可以在后台自由地创造奇迹,提高性能,而不需要调用者关心。