【5min+】 秋名山的竞速。 ValueTask 和 Task

系列介绍

简介

【五分钟的dotnet】是一个利用您的碎片化时间来学习和丰富.net知识的博文系列。它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net知识等等。

场景

您可以在下班坐地铁的时候,拿出手机逛一逛博客园,利用短短的五分钟完成阅读。

诞生缘由

  • 曾经学过的内容可能过不了多久就忘了,我们需要一些文章来帮我们查漏补缺。
  • 太长篇幅的文章看着滚动条就害怕了,我们可能更期望文字少的文章。
  • .net体系的内容太多了,平时也不知道该学哪些,我们可能需要一点点知识线索。

文章质量

当然,并不意味着它篇幅短就质量差。所谓麻雀虽小五脏俱全,我们会尽可能保证利用最少的文字去详细的阐述内容。

正文

伴随着dotnet core的不断迭代,我们在享受.net性能上的提升之外,还收获了许许多多新出现的API。不知您有没有发现,有这样一个类型在开始逐渐出现在我们的视野中 ———— ValueTask

比如在最新的EF Core中:

public virtual async ValueTask<EntityEntry> AddAsync(
    object entity,
    CancellationToken cancellationToken = default)

以上代码是EF Core中DBContext的AddAsync签名,我们可以发现它的返回类型为ValueTask,可能就如同您想的一样,既然AddAsync是这样,那异步查找方法返回的类型也是这样。是的,曾经这些由Task来包裹的结果,现在全部交由VauleTask来处理了。

在最新的C# 8的特性中,引入了 异步流 的概念。它在原有的同步迭代器的基础上,扩充了异步的迭代器版本:

IAsyncEnumerableIAsyncEnumerator

如果您还不了解同步的迭代器接口,可以查看本系列的 上一篇文章

而这个异步迭代器接口的方法签名是这样的:

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:

  • Awaiting the instance multiple times.
  • Calling AsTask multiple times.
  • Using .Result or .GetAwaiter().GetResult() when the operation hasn‘t yet completed, or using them multiple times.
  • Using more than one of these techniques to consume the instance.

原文地址:https://www.cnblogs.com/uoyo/p/12188739.html

时间: 2024-11-11 15:38:31

【5min+】 秋名山的竞速。 ValueTask 和 Task的相关文章

【5min+】你怎么穿着品如的衣服?IEnumerable AND IEnumerator

系列介绍 简介 [五分钟的dotnet]是一个利用您的碎片化时间来学习和丰富.net知识的博文系列.它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net知识等等. 场景 您可以在下班坐地铁的时候,拿出手机逛一逛博客园,利用短短的五分钟完成阅读. 诞生缘由 曾经学过的内容可能过不了多久就忘了,我们需要一些文章来帮我们查漏补缺. 太长篇幅的文章看着滚动条就害怕了,我们可能更期望文字少的文章. .net体系的内容太多了,平时也不知道该学哪些,我们可

C#7的新语法

C#7为C#语言添加了许多新功能: out变量: 您可以将out内联值声明为使用它们的方法的参数. 元组 您可以创建包含多个公共字段的轻量级,未命名的类型.编译器和IDE工具可以理解这些类型的语义. 模式匹配 您可以根据这些类型的成员的任意类型和值创建分支逻辑. ref 当地人和回报 方法参数和局部变量可以是对其他存储的引用. 本地功能 您可以将功能嵌套在其他功能中,以限制其范围和可见性. 更健康的成员 可以使用表达式创作的成员列表已经增加. throw 表达式 您可以在以前不允许的代码结构中抛

async/await使用深入详解

async和await作为异步模型代码编写的语法糖已经提供了一段时间不过一直没怎么用,由于最近需要在BeetleX webapi中集成对Task方法的支持,所以对async和await有了深入的了解和实践应用.在这总结一下async和await的使用,主要涉及到:自定义Awaitable,在传统异步方法中集成Task,异常处理等. 介绍 在传统异步方法处理都是通过指定回调函数的方式来进行处理,这样对于业务整非常不方便.毕竟业务信息和状态往往涉及到多个异步回调,这样业务实现和调试成本都非常高.为了

HttpClient Received an unexpected EOF or 0 bytes from the transport stream

请求https链接时报错,奇怪的是pc1正常,pc2异常 Unhandled Exception: System.AggregateException: One or more errors occurred. ( Received an unexpected EOF or 0 bytes from the transport stream.) ---> System.IO.IOException: Received an unexpected EOF or 0 bytes from the t

C#中await/async闲说

原文:C#中await/async闲说 自从C#5.0增加异步编程之后,异步编程越来越简单,async和await用的地方越来越多,越来越好用,只要用异步的地方都是一连串的异步,如果想要异步编程的时候,需要从底层开始编写,这样后边使用的时候就是异步,那么底层是如何实现??我们如何编写高效率的异步方法?? #了解基于任务的异步模式(TAP) 基于任务的异步编程模型 (TAP) 提供了异步代码的抽象化,你只需像往常一样将代码编写为一连串语句即可,在开始调用的地方运行.例如:var task = me

【5min+】帮我排个队,谢谢。await Task.Yield()

系列介绍 [五分钟的dotnet]是一个利用您的碎片化时间来学习和丰富.net知识的博文系列.它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net知识等等. 5min+不是超过5分钟的意思,"+"是知识的增加.so,它是让您花费5分钟以下的时间来提升您的知识储备量. 正文 如果您现在正在使用.NetCore的话,相信您对await 和 async这两个关键字再熟悉不过了.它们是为异步编程提供的语法糖,便于我们在代码中更便捷的进行异步

【5min+】传说中的孪生兄弟? Memory and Span

系列介绍 [五分钟的dotnet]是一个利用您的碎片化时间来学习和丰富.net知识的博文系列.它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net知识等等. 5min+不是超过5分钟的意思,"+"是知识的增加.so,它是让您花费5分钟以下的时间来提升您的知识储备量. 正文 在上一篇文章:<闪电光速拳? .NetCore 中的Span> 中我们提到了在.net core 2.x 所新增的一个类型:Span. 它与咱们传统使

Conductor Task Workers

由远程worker执行的conductor任务通过HTTP端点进行通信以轮询任务并更新执行状态. conductor提供了轮询任务的框架,管理执行线程并将执行状态更新回服务器.该框架提供了Java和Python中的库.可以通过使用用于任务管理的HTTP端点来添加其他语言支持. Java 实现Worker接口来实现任务.https://github.com/Netflix/conductor/blob/dev/client/src/main/java/com/netflix/conductor/c

C#,Task

1.Task.Run(); static void Main(string[] args) { long a = 9876545678, b = 987654567892; Task task = Task.Run(() => { Add(a, b); }); Console.ReadLine(); } static void Add(long a,long b) { Console.WriteLine("This is the Add method!"); Console.Wr