【五分钟的dotnet】是一个利用您的碎片化时间来学习和丰富.net知识的博文系列。它所包含了.net体系中可能会涉及到的方方面面,好比C#的小细节,AspnetCore,微服务中的.net知识等等。html
您能够在下班坐地铁的时候,拿出手机逛一逛博客园,利用短短的五分钟完成阅读。数据库
固然,并不意味着它篇幅短就质量差。所谓麻雀虽小五脏俱全,咱们会尽量保证利用最少的文字去详细的阐述内容。c#
伴随着dotnet core的不断迭代,咱们在享受.net性能上的提高以外,还收获了许许多多新出现的API。不知您有没有发现,有这样一个类型在开始逐渐出如今咱们的视野中 ———— ValueTask。api
好比在最新的EF Core中:缓存
public virtual async ValueTask<EntityEntry> AddAsync( object entity, CancellationToken cancellationToken = default)
以上代码是EF Core中DBContext的AddAsync签名,咱们能够发现它的返回类型为ValueTask,可能就如同您想的同样,既然AddAsync是这样,那异步查找方法返回的类型也是这样。是的,曾经这些由Task来包裹的结果,如今所有交由VauleTask来处理了。异步
在最新的C# 8的特性中,引入了 异步流 的概念。它在原有的同步迭代器的基础上,扩充了异步的迭代器版本:async
IAsyncEnumerable 和 IAsyncEnumerator微服务
若是您还不了解同步的迭代器接口,能够查看本系列的 上一篇文章。性能
而这个异步迭代器接口的方法签名是这样的:学习
public interface IAsyncEnumerator<out T> : IAsyncDisposable { T Current { get; } ValueTask<bool> MoveNextAsync(); }
OMG,又是ValueTask!!!
那么,ValueTask究竟是什么东西呢?它和传统的Task又有什么区别呢?该在何时使用它。
不要慌,接下来的五分钟您将Get到它。
在开始以前,咱们先来了解一下我们.NET中对内存中对象的存储格式:堆与栈。
先来看栈和堆的区别:
而在C#里面(其它.NET语言同理哈),我们都知道有Class 和 Struct这两个类别,这两个类别对应的就是引用类型和值类型。
咱们先拿实例化一个类来讲,好比咱们在执行 var newInstance = new ClassA()的时候,咱们就会创建一个A的对象,而这个对象的数据通常来讲就是分配在堆上的,而同时会创建一个引用ID,该ID就通常就置放在栈上面。
那么值类型的数据呢?通常来讲它是存放在栈上的。固然这句话不全对:
"值类型存储在栈中, 引用类型存储在堆中” 这句话的前半句是有争议的,“变量的值是在它声明的位置存储的,假如一个类中有一个int类型的实例变量,那么在这个类的任何对象中,该变量的值老是和对象中的其余数据在一块儿,也就是在堆上,只有局部变量(方法内部声明的变量)和方法参数在栈上。而对于C#2以及更高版本,不少局部变量并不彻底存放在栈中”引用-《C# in depth》及译本《深刻理解C#》.
这也是为何咱们会将结构化的小数据建立为Struct的缘由,好比具备(R,G,B)三个属性的结构Color。
栈里面的数据通常来讲由于空间小,读取数据库的缘由,它的生命周期就比较小,好比一个返回值为int的方法,当方法完成以后,该栈中的数据就销毁了。而堆呢?堆保存了几乎全部类中的数据,它怎么销毁数据来保存内存不溢出呢? 是的,您会想到GC,在.NET中就是一个专门的垃圾回收器来完成该操做。
回到本篇文章的主题,ValueTask。 Task可能你们都用的比较多了,毕竟从DotNET Framework的年代就流传至今,而ValueTask却从DotNET Core2.0才引入。
咱们先来看看 MSDN 中对ValueTask的阐述:
提供异步操做的可等待结果。提供包装 Task 和 TResult(仅使用其中之一)的值类型。
往下滑MSDN,就能看到里面有一个很重要的一点:
There are tradeoffs to using a ValueTask
instead of a Task . For example, while a ValueTask can help avoid an allocation in the case where the successful result is available synchronously, it also contains multiple fields, whereas a Task as a reference type is a single field.
不要问为何这个是英文,由于我尝试MSDN的机翻。唉…………能读懂个鬼,强烈建议给MSDN负责翻译的人员扣鸡腿。
上面大体的意思就是说,ValueTask会避免同步状况下一些没必要要的内存分配,从而提高应用总体的性能。
因此说,如今就能明白ValueTask出现的目的是为了提高性能,而被提高的对象就是Task。二位秋名山车神的竞速之路:
若是您足够仔细,您会发现我上面说的是同步的状况。 “???纳尼,我用Task不是异步吗?怎么成同步了?”
别急,回想下您是否写过这样的代码:
return Task.FromResult(42);
您确定写过(就算没写过也看过😝),那么为何会有这种代码呢? 由于咱们须要在方法中部分执行异步,而后使用awit关键字等待它返回一个肯定的结果,而后进行一波同步运算后返回结果。
因此如今问题就来了,之前的版本咱们都是这样写,这没有一点问题,可是咱们须要明白一点:Task是一个类,开胃菜中咱们得知了,类在实例化的时候数据量会被存放在堆中,等待没有引用以后就被GC回收掉。因此来看刚才那句代码,咱们的返回类型是什么,一个Task<int>。
若是咱们以同步方式来实现直接返回一个int是什么样呢? 数据被置放在栈中,方法完成后,内存中的数据就消失了。这个周期很是的短,并且内存分配极小。
那么使用Task以后呢,实例化了一个Task对象,放在堆中,堆里面置放了大量的Task缓存对象。直至最后来等待GC回收。
来看看下面这个代码:
public async Task<int> ReadNextByteAsync() { if (_bufferedCount == 0) { await FillBuffer(); } if (_bufferedCount == 0) { return -1; } _bufferedCount--; return _buffer[_position++]; }
假如它是一个读取文本的方法,则ReadNextByteAsync可能会被调用无数次,好比10w+,那么按照咱们的猜测结果是什么样子呢? 内存堆中存了10w+被包裹的Task对象数据。哪怕一个Task的所须要的内存量极小,那么10w+以后会是什么样呢?那么1000w+呢?
OMG,不敢想象。(隔壁:128G海盗船请求出战)
因此,救星来了:ValueTask。它从遥远的M78星云…………哦,不对,它从.NET Core2.0中出现了。
public readonly struct ValueTask : IEquatable<ValueTask> { }
是的,它就是这个样子。它是一个结构体,也就是值类型。若是按照咱们以前对值类型和引用类型的说法来猜测,使用ValueTask完成上面的ReadNextByteAsync是什么样子呢? 它将数据存放在栈中,每次方法结束后它将被释放,避免没必要要的内存开销。
因此这也是以前MSDN上说的,在同步中它会提升性能的缘由。
因此之后咱们能够尝试将如下代码替换:
//brefore return Task.FromResult(42); //after return new ValueTask(42);
ValueTask 被讲的这么好,是否是全部用Task的地方均可以用ValueTask了呢?
若是真的要回答这个问题的话,答案是:不是的。
回到MSDN对它的定义,您会发现,它是对Task的包装。由于是包装的缘由,因此您可将全部用Task的地方转换为ValueTask,编译器并不会报错,并且ValueTask还能够转换为Task,毕竟是个包装器嘛。
来看看ValueTask的源码:
也就是说若是咱们不是经过同步的方式直接获得结果,而是对Task的包装的话,获取ValueTask结果的内部其实仍是经过Task在进行操做。
因此若是异步操做须要返回Task的状况下,咱们将返回值更改成ValueTask反而增大了内存存储量(有一个Task对象,又有一个ValueTask对象)。
那么?异步的状况我也想避免没必要要的开销怎么办呢? 可能您发现了在ValueTask里面还出现了另外的一个东西:IValueTaskSource 。
本文因为篇幅有限,因此只能在后期为你们带来该接口的介绍(虽然有点吊胃口哈,不过这确实超纲了😭),若是您有兴趣,能够参考如下Stephen在文中所说起到的有关IValueTaskSource的部分。
因此,到这里咱们大体了解了ValueTask出现的目的,以及它和Task的区别。您能够想一下,您如今所作的项目中是否能够引入ValueTask,虽然它在core 2.0以后引入,可是因为.NET standard的特性,core和farmework都可以使用它。
如下是ValueTask使用的一些误区,是摘自MSDN,若是您看懂了本篇的内容,您就能够很容易知道它为何不能这么使用。对了,因为MSDN翻译问题,因此这里仍是英文。(再次说一句,翻译扣鸡腿)。
对了,小声说一句:“嘘,点波关注哈”
The following operations should never be performed on a ValueTask<TResult> instance: