一个 Task 不够,又来一个 ValueTask ,真的学懵了!

一个 Task 不够,又来一个 ValueTask ,真的学懵了!,第1张

概述一:背景 1. 讲故事 前几天在项目中用 MemoryStream 的时候意外发现 ReadAsync 方法多了一个返回 ValueTask 的重载,真是日了狗了,一个 Task 已经够学了,又来一个 一:背景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 -lOS Thread ID: 0x44cc (0)        Child SP               IP Call Site0000004DA3B7E630 00007ffaf84329a6 ConsoleApp2.Program.Main(System.String[]) [E:\net5\ConsoleApp1\ConsoleApp2\Program.cs @ 17]    LOCALS:        0x0000004DA3B7E6E8 = 0x000001932896ac78        0x0000004DA3B7E6E0 = 0x000001932897e7000:000> !obJsize 0x000001932896ac78sizeof(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 0x000001932897e700sizeof(000001932897E700) = 16056 (0x3eb8) bytes (System.Collections.Generic.List`1[[System.Threading.Tasks.ValueTask`1[[system.int32,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,cursor);            }        }    }

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

0:000> !dumpheap -type Task -statStatistics:              MT    Count    TotalSize Class name00007ffaf2404650        1           24 System.Threading.Tasks.Task+<>c00007ffaf24042b0        1           40 System.Threading.Tasks.TaskFactory00007ffaf23e3848        1           64 System.Threading.Tasks.Task00007ffaf23e49d0        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 ,真的学懵了!全部内容,希望文章能够帮你解决一个 Task 不够,又来一个 ValueTask ,真的学懵了!所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/1213793.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-04
下一篇 2022-06-04

发表评论

登录后才能评论

评论列表(0条)

保存