Net4.6 Task 异步函数 比 同步函数 慢5倍 踩坑经历

https://www.cnblogs.com/shuxiaolong/p/DotNet_Task_BUG.html

异步Task简单介绍
本标题有点 哗众取宠,各位都别介意(不排除个人技术能力问题) —— 接下来:我将会用一个小Demo 把 本文思想阐述清楚。

.Net 4.0 就有了 Task 函数 —— 异步编程模型

.Net 4.6 给 Task 增加了好几个 特别实用的方法,而且引入了 await async 语法糖

当然,这是非常不错的技术,奈何我有自己的线程队列封装,也就没有着急使用这个东西。

终究入局 Task异步函数
近来,有项目需要使用到 DotNetty 这个异步Socket框架。

这个框架是 微软团队 移植的 Java的 Netty —— 而且还能与 Java 现有的 Netty 对接。

Netty 如何的牛逼 我就不多介绍了。

DotNetty 基于 .Net 4.3 (实际至少需要 .Net 4.5) —— 是的,你没有看错,是 .Net 4.3

好了,跟着我一起踩坑,一起学些 异步Task函数的 使用规范。

先看一个最简单的 Demo,领教一下 Task 的异步威力
复制代码
1 static void Main(string[] args)
2 {
3 //模拟一个业务需求: 有 200 个字符串需要处理
4 List list = new List();
5 for (int i = 0; i < 200; i++) list.Add("AAAA" + i);
6
7
8
9 DateTime time0 = DateTime.Now;
10
11 //用多个Task 处理这些字符串
12 List listTask = new List();
13 foreach (string item in list)
14 {
15 Task task = Task.Run(() =>
16 {
17 Handle(item); //执行一个方法, 处理这200多个字符串
18 });
19 listTask.Add(task);
20 }
21 Task.WaitAll(listTask.ToArray()); //等待200个字符串 都处理完成
22
23 DateTime time1 = DateTime.Now;
24
25
26
27 Console.WriteLine("200个字符串处理完成, 同步执行需要200秒, 实际Task执行耗时: " + (time1 - time0).TotalSeconds + " 秒");
28
29
30
31 }
32
33
34 public static void Handle(string item)
35 {
36 Thread.Sleep(1000); //处理耗时1秒
37 Console.WriteLine("处理 " + item);
38 }
复制代码

业务的处理逻辑没这么简单
实际上,我们有 AAAA0 ~ AAAA199 总计 200 个字符串

但是,实际处理字符串 需要一个 StrHandler 类。

并且,StrHandler 有个属性 Type, 如果 StrHandler.Type==1,则这个 StrHandler 就只能处理 AAAA1 AAAA11 .... AAAA191 这些以 1 结尾的字符串

那么:200个 字符串 就需要 最少10个 StrHandler 来处理。 理论:200个 字符串,创建 200个 StrHandler 来处理 不就得了? 但是:StrHandler 的 构造函数有一些 初始化操作,非常耗时,需要 5秒。

我们先看一下 new 200 个 StrHandler 会有多慢。 如果使用同步函数,那就是 (1+5)*200 = 1200 秒

复制代码
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 //模拟一个业务需求: 有 200 个字符串需要处理
6 List list = new List();
7 for (int i = 0; i < 200; i++) list.Add("AAAA" + i);
8
9
10
11 DateTime time0 = DateTime.Now;
12
13 //用多个Task 处理这些字符串
14 List listTask = new List();
15 foreach (string item in list)
16 {
17 Task task = Task.Run(() =>
18 {
19 Handle(item); //执行一个方法, 处理这200多个字符串
20 });
21 listTask.Add(task);
22 }
23 Task.WaitAll(listTask.ToArray()); //等待200个字符串 都处理完成
24
25 DateTime time1 = DateTime.Now;
26
27
28
29 Console.WriteLine("200个字符串处理完成, 同步执行需要200秒, 实际Task执行耗时: " + (time1 - time0).TotalSeconds + " 秒");
30
31
32
33 }
34
35
36 public static void Handle(string item)
37 {
38 //字符串最末位的数字 就是 StrHandler 的 Type 值
39 int temp = Convert.ToInt32(item.Substring(item.Length - 1));
40
41 StrHandler handler = new StrHandler(temp);
42 handler.Handle(item);
43 }
44
45 }
46
47 //字符串的 处理类
48 public class StrHandler
49 {
50 public StrHandler(int type)
51 {
52 Type = type;
53 Thread.Sleep(5000); //创建一个 StrHandler 需要5秒
54 }
55
56 public int Type { get; set; }
57
58
59 public void Handle(string item)
60 {
61 Thread.Sleep(1000); //函数本身调用需要 1秒钟
62 Console.WriteLine("处理器 {0} 处理字符串 {1}", Type, item);
63 }
64 }
复制代码

复用 StrHandler 减少开销
因为 StrHandler 需要创建 Tcp 通讯信道,开辟多个将占用不必要的网络端口。 200个字符串,最少需要 10个 StrHandler —— 所以:我们就只创建 10个 StrHandler。

我们修改 static Handle() 函数如下

复制代码
1 //public static void Handle(string item)
2 //{
3 // int temp = Convert.ToInt32(item.Substring(item.Length - 1)); //字符串最末位的数字 就是 StrHandler 的 Type 值
4
5 // StrHandler handler = new StrHandler(temp);
6 // handler.Handle(item);
7 //}
8
9 public static void Handle(string item)
10 {
11 StrHandler handler = GetHandler(item);
12 handler.Handle(item);
13 }
14
15 private static Hashtable hash = Hashtable.Synchronized(new Hashtable());
16
17 //不同的字符串有不同的 StrHandler
18 //StrHandler 是一种昂贵资源, 初始化 StrHandler 需要5秒, 所以需要 对 GetHandler 进行缓存
19 private static StrHandler GetHandler(string item)
20 {
21 //lock (hash) //加不加lock 不影响 本文最终理论
22 {
23 int temp = Convert.ToInt32(item.Substring(item.Length - 1));
24
25 StrHandler handler = hash[temp] as StrHandler;
26 if (handler != null) return handler;
27
28 //如果没有缓存, 则创建 StrHandler
29 handler = new StrHandler(temp);
30 hash[temp] = handler;
31 return handler;
32 }
33 }
复制代码

我们使用 Hashtable 缓存了 StrHanlder 类 —— 再看一下性能

StrHandler 初始化 DotNetty 通讯
上面的几次演变,把性能逐步提高了不少。 业务要求: StrHandler 需要和 DotNetty 通讯,在 调用 StrHandler 的 Handle(string) 之前,就必须让 DotNetty 完成初始化。

看一下改进后的 StrHandler

复制代码
1 //字符串的 处理类
2 public class StrHandler
3 {
4 public StrHandler(int type)
5 {
6 Type = type;
7 DotNetty = new DotNetty();
8
9 //Thread.Sleep(5000);
10 DotNetty.Start(); //耗时的 5秒, 其实就是 DotNetty 的时间消耗 //调用的是假设的同步方法
11 }
12
13 public int Type { get; set; }
14 public DotNetty DotNetty { get; set; } //增加了 DotNetty 的类(这可是一个重量级对象,可不是 说new就new 的)
15
16 //函数本身调用需要 1秒钟
17 public void Handle(string item)
18 {
19 if (!DotNetty.Active)
20 throw new Exception(string.Format("{0} DotNetty 没有激活, 无法执行 Handle", Type));
21
22 Thread.Sleep(1000);
23 Console.WriteLine("处理器 {0} 处理字符串 {1}", Type, item);
24 }
25 }
26 我们再看一下 DotNetty 的定义(模拟定义)
27
28 //以下代码模拟 DotNetty 框架 —— 这个框架 只提供了 异步Task 方法 StartAsync();
29 //所以: StartAsync() 定义不能修改
30 public class DotNetty
31 {
32 //DotNetty 只提供了 异步函数
33 public async Task StartAsync()
34 {
35 await Task.Run(() =>
36 {
37 //DotNetty 是一个著名的通讯框架, 正常情况下 初始化只需要1秒。
38 //但 特殊情况下,初始化需要 5秒 (比如 目标的 IP端口 压根不存在)
39 Thread.Sleep(5000);
40 });
41
42 Active = true; //DotNetty 初始化完成还有, 将 DotNetty 置为激活状态
43 }
44
45 //假设给 DotNetty 提供一个 同步的 Start() 方法
46 //实际上: DotNetty 没有这个同步方法
47 public void Start()
48 {
49 Thread.Sleep(5000);
50 Active = true; //DotNetty 初始化完成还有, 将 DotNetty 置为激活状态
51 }
52
53 public bool Active { get; set; }
54 }
复制代码

DotNetty 不提供 Start() 方法,我们假设增加一个 同步方法 Start()

—— 这次测试的是 假设有个 同步函数 Start() 的性能。

DotNetty 只提供异步Task方法 StartAsync()
我们上面也说了,DotNetty 只提供 StartAsync() 这个方法。

我们刚才模拟的 Start() 是不存在的。

这时候,有经验的小伙伴 一定能指出来:

没有提供同步函数,我们可以把 异步Task 函数 封装成 同步函数啊!

说的很对,我们可以给 DotNetty 扩展一个 Start() 方法,

复制代码
1 public static class Extend
2 {
3 public static void Start(this DotNetty dotNetty)
4 {
5 Task task = dotNetty.StartAsync();
6 task.Wait(); //让 异步Task 等待完成, 这不就是一个 同步方法了么?
7 }
8 }
复制代码
为了证实猜想,我还特意 写了个 测试代码。

复制代码
1 static void Main(string[] args)
2 {
3 DotNetty dotNetty = new DotNetty();
4 dotNetty.Start();
5 Console.WriteLine("DotNetty.Active : " + dotNetty.Active);
6 }
复制代码

增加了扩展方法之后,程序编译通过了

正式运行

本文总结
本文,通过一个简单的 Demo,演示了 如何将 Task异步编程 搞死的案例。

终究得出了如下结论:

Task异步函数 通过 Wait() 封装的 伪同步函数 是靠不住的。

Task.WaitAll() 函数 是最大的坑 (这是 .Net 4.6 新增加的函数?)

DotNetty 不提供 同步函数 Start(),只提供 StartAsync() 是不厚道的。

建议:所有底层库,你可以有 Task函数,但请保留 同步函数。

绝不小心求证、只管大胆胡说
这个段落,可以当作开玩笑 —— 各位不要较真。

复制代码
PS.
以前我们写函数, 会准备 同步函数、回调函数
.Net 4.5 后, 引入了 异步函数模型

上面的案例中, 我们看到: 一个异步Task 方法, 既能当 回调函数用, 又能当同步函数用
—— 你或许觉得: 异步Task方法 好强大

但是, 警告建议:

无数网友(包括大神) 异步Task 踩坑经历, 包括自己这次的 踩坑,
都得出了一个结论: 异步Task只能一条道走到黑

即: 你一个地方使用了 异步Task, 其他引用的地方 往上, 都得改成 异步.
你反驳: 我可以把 异步Task 封装成 一个同步函数啊, task.Wait(); return task.Result;
—— 这就是你踩坑的开始: 你可能会看到 线程飙升到 900个, 但是 CPU利用率为 0%
—— 于是最后, 你就会回到最开始的建议: 异步Task只能一条道走到黑
—— 900个线程, CPU 0%, 如何查错就是问题了: 每一个函数都看起来没问题(是真的没问题), 串在一起运行后 就假死
—— 你以为是 死锁? 可能几分钟后 900个线程 全部瞬间又运行起来了
—— 并没有死锁, 似乎就是 Task 内核实现的一种资源分配 BUG(为了不导致死锁,所以才飙升900个线程的资源分配方案)

Task异步函数 是强大的, 但请不要滥用

同步函数 封装成 异步函数 不会有任何问题
但是Task异步函数 封装成 同步函数(就是伪同步函数) —— 这会是你噩梦的开始

有个直觉猜想(可能不正确):
A() 是个内核异步Task函数 开辟了10个Task做事(做完就全部释放),
B() 是个底层异步Task函数, 因为某些原因, B() 调用 A()的伪同步函数, B开辟了10个Task做事
C() 是个上层调用函数, 他调用了 B()的伪同步函数, C开辟了10个Task做事
—— 最后,一旦假死发生, 线程数或许会等于 101010 = 1000 个, 或者 2000 个
—— 假死发生时的 线程数, 和异步Task的伪同步函数 嵌套层次 有关系
—— 设想一下, A() 引用了 NLog 这种更内核的库, 哪天 NLog 作者将自己的代码改成了异步Task, A() 为了代码改动最小, 封装了一个 NLog 的 伪同步函数(保持了之前代码调用的一致性), 假设NLog开辟了5个Task
51010*10 = 5000 个线程 估计是逃不掉了 【个人乱猜,都别介意】

日月轮转、沧海桑田 —— 可以提供 Task异步函数, 但尽量同时保留 同步函数(尤其是底层框架)
复制代码

写在最后的话
总有程序员,能理解 同步函数、勉强理解 回调函数,完全不懂 异步函数 —— 完全是在模仿着写 await async。

作为一个底层 架构师,如果你的底层 都是高大上 的异步函数 —— 会不会让使用你的框架的 开发人员 也遇到今天这样的 BUG呢?

同步函数 就像 亲力亲为, 回调函数 就像 软件外包, 异步函数 就像 管理一家大公司 —— 人多力量大的同时,如果管理不好,就可能发生 一件小事 几个人做,结果还扯皮 的尴尬 ~

Net4.6 Task 异步函数 比 同步函数 慢5倍 踩坑经历的更多相关文章

  1. 『审慎』.Net4.6 Task 异步函数 比 同步函数 慢5倍 踩坑经历

    异步Task简单介绍 本标题有点 哗众取宠,各位都别介意(不排除个人技术能力问题) —— 接下来:我将会用一个小Demo 把 本文思想阐述清楚. .Net 4.0 就有了 Task 函数 —— 异步编 ...

  2. JAVA之旅(十三)——线程的安全性,synchronized关键字,多线程同步代码块,同步函数,同步函数的锁是this

    JAVA之旅(十三)--线程的安全性,synchronized关键字,多线程同步代码块,同步函数,同步函数的锁是this 我们继续上个篇幅接着讲线程的知识点 一.线程的安全性 当我们开启四个窗口(线程 ...

  3. QT 异步函数转为同步函数的方法

    在QT中,一般推荐使用异步函数.除了异步函数的非阻塞特性外,QT的Signal/Slot特性在异步函数中可以得到充分的发挥.因此,在QT中,很多API的设计都是使用非阻塞的异步函数作为API,然后执行 ...

  4. MQ异步同步搜索引擎ElasticSearch数据踩坑

    业务背景 在大型网站中,为了减少DB压力.让数据更精准.速度更快,将读拆分出来采用搜索引擎来为DB分担读的压力,ElasticSearch就是目前市面上比较流行的搜索引擎,他的检索速度奇快.支持各种复 ...

  5. js回调函数以及同步与异步

    1. 背景介绍javascript的单线程特性由于javascript语言是一门“单线程”的语言,所以,javascript就像一条流水线,仅仅是一条流水线而已,要么加工,要么包装,不能同时进行多个任 ...

  6. mfc 调用Windows的API函数实现同步异步串口通信(源码)

    在工业控制中,工控机(一般都基于Windows平台)经常需要与智能仪表通过串口进行通信.串口通信方便易行,应用广泛. 一般情况下,工控机和各智能仪表通过RS485总线进行通信.RS485的通信方式是半 ...

  7. nodejs 代码设计模式1:同步函数变异步

    同步函数变异步 1 问题: 1.1 碰到需要调用你刚正在创建的对像. function createServer(data, cb) { data.num = 1; cb(); return data ...

  8. OC 线程操作 - GCD使用 -同步函数,异步函数,串行队列,并发队列

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // GCD 开几条线程并不是我们 ...

  9. js中ajax连接服务器open函数的另外两个默认参数get请求和默认异步(open的post方式send函数带参数)(post请求和get请求区别:get:快、简单 post:安全,量大,不缓存)(服务器同步和异步区别:同步:等待服务器响应当中浏览器不能做别的事情)(ajax和jquery一起用的)

    js中ajax连接服务器open函数的另外两个默认参数get请求和默认异步(open的post方式send函数带参数)(post请求和get请求区别:get:快.简单 post:安全,量大,不缓存)( ...

随机推荐

  1. UVA 11090 判负圈问题

    题目链接http://vjudge.net/problem/viewProblem.action?id=34650 题目大意: 给定n个点m条边的加权有向图,求平均权值最小的回路.平均权值=路径权值之 ...

  2. xftp向ubuntu传输文件错误

    xftp向ubuntu传输文件错误原因: 登陆用户对文件夹没有权限. 解决方法:授予权限 chmod 777 该目录名

  3. [USACO13NOV]空荡荡的摊位Empty Stalls

    题目描述 Farmer John's new barn consists of a huge circle of N stalls (2 <= N <= 3,000,000), numbe ...

  4. 【BZOJ1031】字符加密Cipher(后缀数组)

    题意:将一个长度为2n(复制粘贴后)的字符串的所有长度为n的后缀从小到大排序,并依次输出它们的最后一个字母. n<=100000 思路:裸SA,模板真难背 P党不得不写成C++风格 ..]of ...

  5. BZOJ1704: [Usaco2007 Mar]Face The Right Way 自动转身机

    n<=5000个数0或1,每次可以连续对固定长度区间取反,目标把所有1变0,求一个取反区间的固定长度K使取反次数最少. 答案关于K不单调,因此枚举K,对每个K扫一遍区间,遇到1就把连续K个数反转 ...

  6. 【cmd】cmd常用命令

    dir 是英文单词directory(目录)的缩写,主要用来显示一个目录下的文件和子目录 md  是英文make directory(创建目录)的缩写 cd  是英文change directory( ...

  7. 字符串常量与const常量内存区(——选自陈皓的博客)

    1. 一个常见的考点: char* p = "test"; 那么理利用指针p来改变字符串test的内容都是错误的非法的. 例如: p[0] = 's'; strcpy(p, &qu ...

  8. “亚信科技杯”南邮第七届大学生程序设计竞赛之网络预赛 A noj 2073 FFF [ 二分图最大权匹配 || 最大费用最大流 ]

    传送门 FFF 时间限制(普通/Java) : 1000 MS/ 3000 MS          运行内存限制 : 65536 KByte总提交 : 145            测试通过 : 13 ...

  9. 小米自动砸蛋机器js代码

    02 //地址:http://static.xiaomi.cn/515 03 //@author:liuzh 04 //@url:http://blog.csdn.net/isea533 05 var ...

  10. 动态规划:Ignatius and the Princess IV

    #include<stdio.h> #include<string.h> #include<math.h> int main() { _int64 n,a; whi ...