一:背景

1. 讲故事

前几天在项目中用 MemoryStream 的时候意外发现 ReadAsync 方法多了一个返回 ValueTask 的重载,真是日了狗了,一个 Task 已经够学了,又来一个 ValueTask,晕,方法签名如下:


public class MemoryStream : Stream
{
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default(CancellationToken))
{
}
}

既然是新玩意,我就比较好奇,看看这个 ValueTask 是个啥玩意,翻翻源码看看类定义:


public readonly struct ValueTask<TResult> : IEquatable<ValueTask<TResult>>
{
}

原来是搞了一个 值类型的Task,无数的优化经验告诉我,值类型相比引用类型要节省空间的多,不信的话可以用 windbg 去校验一下,分别在 List 中灌入 1000 个Task 和 1000 个 ValueTask,看看所占空间大小。


0:000> !clrstack -l
OS Thread Id: 0x44cc (0)
Child SP IP Call Site
0000004DA3B7E630 00007ffaf84329a6 ConsoleApp2.Program.Main(System.String[]) [E:\net5\ConsoleApp1\ConsoleApp2\Program.cs @ 17]
LOCALS:
0x0000004DA3B7E6E8 = 0x000001932896ac78
0x0000004DA3B7E6E0 = 0x000001932897e700
0:000> !objsize 0x000001932896ac78
sizeof(000001932896AC78) = 80056 (0x138b8) bytes (System.Collections.Generic.List`1[[System.Threading.Tasks.Task`1[[System.Int32, System.Private.CoreLib]], System.Private.CoreLib]])
0:000> !objsize 0x000001932897e700
sizeof(000001932897E700) = 16056 (0x3eb8) bytes (System.Collections.Generic.List`1[[System.Threading.Tasks.ValueTask`1[[System.Int32, System.Private.CoreLib]], System.Private.CoreLib]])

上面的代码可以看出, 1000 个 Task 需占用 80056 byte,1000 个 ValueTask 需占用 16056 byte,相差大概 5 倍,空间利用率确实得到了大大提升,除了这个, ValueTask 还想解决什么问题呢?

二:ValueTask 原理分析

1. 从 MemoryStream 中寻找答案

大家可以仔细想一想,既然 MemoryStream 中多了一个 ReadAsync 扩展,必然是现存的 ReadAsync 不能满足某些业务,那不能满足什么业务呢? 只能从方法源码中寻找答案,简化后的代码如下:


public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<int>(cancellationToken);
} int num = Read(buffer, offset, count);
Task<int> lastReadTask = _lastReadTask;
return (lastReadTask != null && lastReadTask.Result == num) ? lastReadTask : (_lastReadTask = Task.FromResult(num));
}

看完这段代码,不知道大家有没有什么疑惑? 反正我是有疑惑的。

2. 我的疑惑

1) 异步 竟然包装了 cpu 密集型操作

C# 引入异步本质上是用来解决 IO 密集型 的场景,利用磁盘驱动器的强势介入进而释放了调用线程,提高线程的利用率和吞吐率,而恰恰这里的 ReadAsync 中的 Read 其实是一个简单的纯内存操作,也就是 CPU 密集型 的场景,这个时候用异步来处理其实没有任何效果可言,说严重一点就是为了异步而异步,或许就是为了统一异步编程模型吧。

2) CPU 密集型处理速度瞬息万里

纯内存操作速度是相当快的,1s内可达千万次执行,那有什么问题呢? 这问题大了,大家看清楚了,这个 ReadAsync 返回的是一个 Task 对象,这就意味着瞬间会在托管堆中生成千万个 Task 对象,造成的后果可能就是 GC 不断痉挛,严重影响程序的性能。

3. 语言团队的解决方案

可能基于我刚才聊到的二点,尤其是第二点,语言团队给出了 ValueTask 这个解决方案,毕竟它是值类型,也就不会在托管堆上分配任何内存,和GC就没有任何关系了,有些朋友会说,空口无凭,Talk is cheap. Show me the code 。

三:Task 和 ValueTask 在 MemoryStream 上的演示

1. Task 的 ReadAsync 演示

为了方便讲解,我准备灌入一段文字到 MemoryStream 中去,然后再用 ReadAsync 一个 byte 一个 byte 的读出来,目的就是让 while 多循环几次,多生成一些Task对象,代码如下:


class Program
{
static void Main(string[] args)
{
var content = GetContent().Result; Console.WriteLine(content); Console.ReadKey();
} public static async Task<string> GetContent()
{
string str = " 一般情况是:学生不在意草稿纸摆放在桌上的位置(他通常不会把纸摆正),总是顺手在空白处演算,杂乱无序。但是,我曾见到有位学生在草稿纸上按顺序编号。他告诉我,这样做的好处是:无论是考试还是做作业,在最后检验时,根据编号,他很快就能找到先前的演算过程,这样大概可以省下两三分钟。这个习惯,可能会跟着他一辈子,他的一生中可以有无数个两三分钟,而且很可能会有几次关键的两三分钟。"; using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(str)))
{
byte[] bytes = new byte[1024]; ms.Seek(0, SeekOrigin.Begin);
int cursor = 0;
var offset = 0;
int count = 1; while ((offset = await ms.ReadAsync(bytes, cursor, count)) != 0)
{
cursor += offset;
} return Encoding.UTF8.GetString(bytes, 0, cursor);
}
}
}

输出结果是没有任何问题的,接下来用 windbg 看一看托管堆上生成了多少个 Task。。。


0:000> !dumpheap -type Task -stat
Statistics:
MT Count TotalSize Class Name
00007ffaf2404650 1 24 System.Threading.Tasks.Task+<>c
00007ffaf24042b0 1 40 System.Threading.Tasks.TaskFactory
00007ffaf23e3848 1 64 System.Threading.Tasks.Task
00007ffaf23e49d0 1 72 System.Threading.Tasks.Task`1[[System.String, System.Private.CoreLib]]
00007ffaf23e9658 2 144 System.Threading.Tasks.Task`1[[System.Int32, System.Private.CoreLib]]
Total 6 objects

从托管堆上看,我去,Task<int> 为啥只有两个呢?,

一个 Task 不够,又来一个 ValueTask ,真的学懵了!的更多相关文章

  1. 瞧一瞧,看一看呐,用MVC+EF快速弄出一个CRUD,一行代码都不用写,真的一行代码都不用写!!!!

    瞧一瞧,看一看呐用MVC+EF快速弄出一个CRUD,一行代码都不用写,真的一行代码都不用写!!!! 现在要写的呢就是,用MVC和EF弄出一个CRUD四个页面和一个列表页面的一个快速DEMO,当然是在不 ...

  2. .NET 中 如果一个Task A正在await另一个Task B,那么Task A是什么状态

    新建一个.NET Core控制台程序,输入如下代码: using System; using System.Threading; using System.Threading.Tasks; class ...

  3. 临远大神,你为啥要建立一个 TASK表。HumanTaskDTO

    临远大神,你为啥要建立一个 TASK表.HumanTaskDTO HumanTask这张表的作用是什么. 为了实现理想中的任务中心.TaskCenter. 首先,工作流可能会完全不包含任何人工节点,全 ...

  4. 在一个由 'L' , 'R' 和 'X' 三个字符组成的字符串(例如"RXXLRXRXL")中进行移动操作。一次移动操作指用一个"LX"替换一个"XL",或者用一个"XR"替换一个"RX"。现给定起始字符串start和结束字符串end,请编写代码,当且仅当存在一系列移动操作使得start可以转换成end时, 返回True。

    在一个由 'L' , 'R' 和 'X' 三个字符组成的字符串(例如"RXXLRXRXL")中进行移动操作.一次移动操作指用一个"LX"替换一个"XL ...

  5. PyCharm在同一个包(package)下,如何把一个.py文件导入另外一个.py文件下

    PyCharm在同一个包(package)下,如何把一个.py文件导入另外一个.py文件下 在同一个包下只需要用import 掉以后就可以找到模块所在的位置,但是如果不在同一个包下,在需要返回父级调用 ...

  6. Entity Framework 6 Recipes 2nd Edition(11-2)译 -> 为一个”模型定义”函数返回一个计算列

    11-3. 为一个”模型定义”函数返回一个计算列 问题 想从”模型定义”函数里返回一个计算列 解决方案 假设我们有一个员工(Employee)实体,属性有: FirstName, LastName,和 ...

  7. 用js把数据从一个页面传到另一个页面

    用js把数据从一个页面传到另一个页面的层里? 如果是传到新页面的话,你网站基于什么语言开发直接用get或者post获取,然后输出到这个层 通过url传参 如果是HTML页面的话JS传到新页面就wind ...

  8. SQL存在一个表而不在另一个表中的数据, 更新字段为随机时间

    --更新字段为随机时间 86400秒=1天 UPDATE dl_robot ), ,GETDATE()) )   SQL存在一个表而不在另一个表中的数据   方法一 使用 not in ,容易理解,效 ...

  9. Struts2从一个action转到另一个action的两种方法

    在Struts2中,Action处理完用户请求后,将会返回一个字符串对象,这个字符串对象就是一个逻辑视图名.Struts 2通过配置逻辑视图名和物理视图之间的映射关系,一旦系统收到Action返回的某 ...

随机推荐

  1. matlab中axis的用法

    来源:https://ww2.mathworks.cn/help/matlab/ref/axis.html?searchHighlight=axis&s_tid=doc_srchtitle a ...

  2. 《C++primerplus》第7章练习题

    1.用户不断输入两个数,计算调和平均数,直到其中一个数为0. #include<iostream> using namespace std; double harm_mean(double ...

  3. Android使用Mono c#分段列表视图

    下载source code - 21.7 KB 你想知道如何把多个ListView控件放到一个布局中,但是让它们在显示时表现正确吗 多个列表项?你对它们正确滚动有问题吗?这个例子将向你展示如何组合单独 ...

  4. Springcloud技术分享

    Springcloud技术分享 Spring Cloud 是一套完整的微服务解决方案,基于 Spring Boot 框架,准确的说,它不是一个框架,而是一个大的容器,它将市面上较好的微服务框架集成进来 ...

  5. java 的 callback

    Java 本身没有回调这一说,但是面向对象可以模拟出来. 1. 回调接口对象 ICommand package com.git.Cmder; public interface ICommand { v ...

  6. 华为方舟编译器正式支持C语言:完全开源

    投递人 itwriter 发布于 2020-10-14 19:08 评论(15) 有1938人阅读 原文链接 2019 年 8 月底,华为方舟编译器(OpenArkCompiler)正式开源,迈出了跨 ...

  7. 远程IO

    远程io 远程io ZLAN6842,ZLAN6844是8路远程O控制器.含有8路DI.8路DO,8路AI输入.其中DI支持干节点和湿节点,带光耦隔离:DO为继电器输出,具有5A 250VAC或5A ...

  8. C++ concurrent_queue

    ConcurrentQueue 用C++11提供的多线程类实现一个线程安全的队列: #include <queue> #include <mutex> #include < ...

  9. selenium元素定位学习笔记

    一,定位原则 稳定 简单灵活 唯一 WebDriver提供了两种方式来定位页面元素,分别是find_element_by_XXX和find_elements_by_XXX.第一种方式的结果是在正常情况 ...

  10. 第十八章 HTTPS介绍及实战演练

    一.HTTPS介绍 1.概述 为什么需要使用HTTPS,因为HTTP不安全,当我们使用http网站时,会遭到劫持和篡改,如果采用https协议,那么数据在传输过程中是加密的,所以黑客无法窃取或者篡改数 ...