网站seo关键词,erp管理系统多少钱,网站导航页面制作,django怎么做网站一#xff1a;背景 1. 讲故事前几天在项目中用 MemoryStream 的时候意外发现 ReadAsync 方法多了一个返回 ValueTask 的重载#xff0c;真是日了狗了#xff0c;一个 Task 已经够学了#xff0c;又来一个 ValueTask#xff0c;晕#xff0c;方法签名如下#xff1a;publ… 一背景 1. 讲故事前几天在项目中用 MemoryStream 的时候意外发现 ReadAsync 方法多了一个返回 ValueTask 的重载真是日了狗了一个 Task 已经够学了又来一个 ValueTask晕方法签名如下public class MemoryStream : Stream{public override ValueTaskint ReadAsync(Memorybyte buffer, CancellationToken cancellationToken default(CancellationToken)){}}既然是新玩意我就比较好奇看看这个 ValueTask 是个啥玩意翻翻源码看看类定义public readonly struct ValueTaskTResult : IEquatableValueTaskTResult{}原来是搞了一个 值类型的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 0x000001932896ac780x0000004DA3B7E6E0 0x000001932897e700
0:000 !objsize 0x000001932896ac78
sizeof(000001932896AC78) 80056 (0x138b8) bytes (System.Collections.Generic.List1[[System.Threading.Tasks.Task1[[System.Int32, System.Private.CoreLib]], System.Private.CoreLib]])
0:000 !objsize 0x000001932897e700
sizeof(000001932897E700) 16056 (0x3eb8) bytes (System.Collections.Generic.List1[[System.Threading.Tasks.ValueTask1[[System.Int32, System.Private.CoreLib]], System.Private.CoreLib]])上面的代码可以看出 1000 个 Task 需占用 80056 byte1000 个 ValueTask 需占用 16056 byte相差大概 5 倍空间利用率确实得到了大大提升除了这个 ValueTask 还想解决什么问题呢二ValueTask 原理分析 1. 从 MemoryStream 中寻找答案大家可以仔细想一想既然 MemoryStream 中多了一个 ReadAsync 扩展必然是现存的 ReadAsync 不能满足某些业务那不能满足什么业务呢只能从方法源码中寻找答案简化后的代码如下
public override Taskint ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{if (cancellationToken.IsCancellationRequested){return Task.FromCanceledint(cancellationToken);}int num Read(buffer, offset, count);Taskint 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 Taskstring 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.Taskc
00007ffaf24042b0 1 40 System.Threading.Tasks.TaskFactory
00007ffaf23e3848 1 64 System.Threading.Tasks.Task
00007ffaf23e49d0 1 72 System.Threading.Tasks.Task1[[System.String, System.Private.CoreLib]]
00007ffaf23e9658 2 144 System.Threading.Tasks.Task1[[System.Int32, System.Private.CoreLib]]
Total 6 objects从托管堆上看我去Taskint 为啥只有两个呢????????了难道我推演错啦不可能的看看源码去。
public override Taskint ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{int num Read(buffer, offset, count);Taskint lastReadTask _lastReadTask;return (lastReadTask ! null lastReadTask.Result num) ? lastReadTask : (_lastReadTask Task.FromResult(num));
}上面最后一句代码不知道大家有没有看懂MemoryStream 用了 _lastReadTask 玩了一个小技巧只要 num 相同返回的都是一个 Task如果不同则会生成新的 Task 对象显然这是根据特定场景进行优化的为了普适性我肯定要绕过这个技巧做法就是每次 num 数字不一样就可以了将 while 修改成代码如下while ((offset await ms.ReadAsync(bytes, cursor, count % 2 0 ? 1 : 2)) ! 0){cursor offset;}然后再用 windbg 看一下
0:000 !dumpheap -type Task -stat
Statistics:MT Count TotalSize Class Name
00007ffaf7f04650 1 24 System.Threading.Tasks.Taskc
00007ffaf7f042b0 1 40 System.Threading.Tasks.TaskFactory
00007ffaf7ee3848 1 64 System.Threading.Tasks.Task
00007ffaf7ee49d0 1 72 System.Threading.Tasks.Task1[[System.String, System.Private.CoreLib]]
00007ffaf7ee9658 371 26712 System.Threading.Tasks.Task1[[System.Int32, System.Private.CoreLib]]
Total 375 objects从最后一行代码可以看到 Count371哈哈这要是千万级的那这里的 Task 有多恐怖可想而知哈。2. ValueTask的 ReadAsync 演示前面例子的危害性大家也清楚了这种场景下解决方案自然就是C#团队提供的新 ReadAsync 方法代码如下class Program{static void Main(string[] args){var content GetContent().Result;Console.WriteLine(content);Console.ReadKey();}public static async Taskstring GetContent(){string str 一般情况是学生不在意草稿纸摆放在桌上的位置他通常不会把纸摆正总是顺手在空白处演算杂乱无序。但是我曾见到有位学生在草稿纸上按顺序编号。他告诉我这样做的好处是无论是考试还是做作业在最后检验时根据编号他很快就能找到先前的演算过程这样大概可以省下两三分钟。这个习惯可能会跟着他一辈子他的一生中可以有无数个两三分钟而且很可能会有几次关键的两三分钟。;using (MemoryStream ms new MemoryStream(Encoding.UTF8.GetBytes(str))){byte[] bytes new byte[1024];Memorybyte memory new Memorybyte(bytes);ms.Seek(0, SeekOrigin.Begin);int cursor 0;var offset 0;var count 1;while ((offset await ms.ReadAsync(memory.Slice(cursor, count % 2 0 ? 1 : 2))) ! 0){cursor offset;}return Encoding.UTF8.GetString(bytes, 0, cursor);}}}很开心用 ValueTask 也实现了同样的功能而且还不给 GC 添任何麻烦不信的话用windbg 校验下
0:000 !dumpheap -type Task -stat
Statistics:MT Count TotalSize Class Name
00007ffaf23f7bf0 1 24 System.Threading.Tasks.Taskc
00007ffaf23f7850 1 40 System.Threading.Tasks.TaskFactory
00007ffaf23c3848 1 64 System.Threading.Tasks.Task
00007ffaf23c49d0 1 72 System.Threading.Tasks.Task1[[System.String, System.Private.CoreLib]]
Total 4 objects0:000 !dumpheap -type ValueTask -stat
Statistics:MT Count TotalSize Class Name
Total 0 objects可以看到托管堆上没有任何踪迹简直就是完美。四ValueTask 真的完美吗 如果真是完美的话我相信底层框架中都会改成 ValueTask而现实并没有也就说明 ValueTask 只是某一些场景下的优选方案如果你明白了上面两个案例你应该会明白 ValueTask 特别适合于那些 CPU 密集型的 异步任务因为是个假异步当你 await 的时候其实结果已经出来了毕竟人家是纯内存操作不和底层的驱动器打交道速度自然相当快。struct 在多线程模式下有很多种限制如果用的不当会有太多的潜在问题和不确定性你可以想一想为啥 lock 锁中大多会用引用类型而不是值类型其实是一样的道理所以它注定是一个高阶玩法相信 95% 的朋友在项目开发中都不会用到用用 Task 就好了基本包治百病 ????????????五总结 从 ValueTask 要解决的问题上可以看出C#语言团队对高并发场景下的性能优化已经快走火入魔了而且现有类库中 99% 的方法还是采用 Task所以普通玩家还是老老实实的用 Task 吧现实中还没有遇到在这个上面碰到性能瓶颈的高能的还是留给高阶玩家吧