无阻塞 编程模型

无阻塞 编程模型 涉及到   异步回调流, Task, async await, 线程池, 并发编程, 并行编程, 大并发架构, 操做系统 之上 编程模型 的 发展   等等  。html

 

我这段时间对 这个领域 的 现状 进行了一些 收集整理 和 批判 , 请看 :程序员

《后线程时代 的 应用程序 架构》  http://www.javashuo.com/article/p-blioukym-ga.html数据库

《我 支持 使用 async await》  http://www.javashuo.com/article/p-kfsmekfd-cv.html编程

 

单纯 从 执行效率 看, 也许 同步方法 最直接, 效率也最高 。 只要 配合 线程池 合理使用 线程 就能够 。服务器

 

异步方法 的 意义 在于 实现 无阻塞 模式, 闭包

而 无阻塞 模式 的 意义 要在 大并发 且    IO 等待时间显著 、IO 可能长时间等待 、 IO 等待时间不肯定(可能有意外)    的时候  才会 体现出来 。架构

什么是 IO 等待 ?     IO 等待 本质上是 CPU 对 外部设备 的 等待 。并发

从 应用 上说, IO 等待 就是  访问数据库, 调用 WebApi,  读写文件,  RPC   等 。异步

 

假设 线程池 有 1000 个 线程,  能够同时处理 1000 个 用户 的 请求,  每一个请求 都 须要 访问数据库,async

若是 数据库 的 查询缓慢,  则 这 1000 个 线程 可能 都会 去等待 数据库, 当有 第 1001 个 以上的 用户 访问 网站 时, 线程池 将 没有 多余 的 线程 去 处理 第 1001 个 以上的 用户 的 请求,  这种状况 若是 持续一段时间,  就会变成 服务器 不能提供 服务,  若是 数据库 处于 “挂掉” 的 异常状态, 则 Web 服务器 线程池 里 的 1000 个 线程 都将 长期 等待数据库 而 挂起,  这样 服务器 就 不能提供 服务,   或者 变得 异常缓慢  (对 用户而言) 。

微服务 的 “雪崩”,  大概 也是 从这里来的 。

 

且 从 广义 的 角度 来说,  线程池 的 1000 个 线程 原本 还能够有一部分 去作 其它 工做(不须要 访问数据库 的 工做,或是 访问 其它数据库 的 工做), 但 都卡在 访问 A 数据库 这里了 。

可是, 咱们 又不能 采用 无限制 的 建立线程(New Thread)的 方式,  过多的 线程 会 花费 比较多的 切换时间,  也会 占用 比较大 的 内存空间, 好比 1 个线程 的 堆栈 是 1 MB,  则 1024 个 线程 的 堆栈空间 总和 就是  1024 * 1 MB  =  1 GB 。

 

因此, 须要 对 线程池 里的 线程 作一个 角色分工 来 解决 这个问题,  这就是   “m  Work,  n   IO” ,

“m  Work,  n   IO”      就是     m 个 工做线程,    n 个 IO 线程   。

 

m 个 工做线程 在 无阻塞 的 状态下工做 。

 

若是是   单核 CPU,  则 能够 退化为    “1  Work,   n   IO”   。

 

若是  1 个 CPU 核 上 只有 1 个 工做线程,  则 称为  “单体”(monosome,  monad) 。

 

Javascript  是 单体  。

 

咱们能够 来 看看  3 种 方式 的 Sequence 图 :

1  调用 同步方法,  如  fileStream.Read() 方法,

2  调用 async 方法  再  task.ContinueWith() , 

3  调用 async 方法,  使用 await,

 

1  调用 同步方法,  如  fileStream.Read() 方法,

           

 

2  调用 async 方法  再  task.ContinueWith() , 

                     

 

3  调用 async 方法,  使用 await,

                                  

 

“状态机”  就是  将  函数参数 、局部变量 等 上下文  保存在 “状态” 中,  将 “状态” 保存在  堆  里,  以 取代 传统的 函数调用 把  参数 、局部变量 等 上下文  保存在  栈  里的 作法 。

假设 有个   Foo()  方法,

 

Foo()

{

        ……        //    Part 1

        await  xxxAsync();

        ……        //    Part 2

}

 

编译器 会将   Foo()  方法 中   await  以前 的 代码 变成一个   Foo_Part1()  方法,  Foo() 方法 中 await 以后 的 代码 变成一个   Foo_Part2()  方法,

这样  Foo()  方法 就被 “分割” 成 3 个 部分 :

1    Foo_Part1()

2    await  xxxAsync()

3    Foo_Part2()

 

在 执行 的 时候,  状态机 就能够 按 “步骤” 调用 这 3 个 部分, 

先调用  Foo_Part1() ,   再调用  xxxAsync(),  以后  转入 异步方法 执行,  本次调用 结束 。

当  xxxAsync()  执行完成后,  会调用 回调,   回调 调用  状态机,   状态机 接着以前的 “步骤”,   继续执行  Foo_Part2() 。

 

这整个 过程 连贯起来,  就是   Foo_Part1() -> xxxAsync() -> Foo_Part2,  这正还原了 程序员 写的 源代码 中的 执行流程 。

程序员 写的 源代码 看起来 是一个  顺序 同步 的 执行过程,  但其实是一个  异步 无阻塞 的 执行过程 。

 

为何要用 状态机 ?    由于要实现 异步架构,  同时还要尽可能 保持 函数层层调用 的 逻辑层次结构 。

好比, 若是 在 执行中 抛出异常, 在 异常信息 中, 能够看到 函数 的 调用层次, 能够看到 异常 是从  “Foo_Part1()”  中 抛出来 的,

这样 咱们 就 清楚 异常 出现 在 那一行代码,

若是 异常 是 从  “Foo_Part2()”  中 抛出来 的,  那咱们也知道 异常 出如今   await  xxxAsync();  以后的 代码 里 。

 

因此,  async await  是一个 语法糖,  有 网友 说是 编译器 的 “黑魔法”,  我总以为  async await  这个 语法糖 有点大, 能够叫 “语法蛋糕” 。

 

而要实现 真正的   “n  IO”  无阻塞,  还须要 操做系统 也用 无阻塞 的 方式 来 实现 IO 。

假设有 n 个 IO 线程,  操做系统 应该 用  1 个 或  n 个 线程 去 “轮流” 等待 多个设备 的 响应 或者 一个设备 对 多个请求 的 响应,

而不该该 固定 1 个 线程 去 等待  1 个 请求 的 响应 。

这种 用 线程 “轮流” 去 等待 设备 响应 的 作法,   就是 IOCP 。

理论上, 只要 CPU 的 处理速度 足够快,  1 个 线程 能够 等待(处理) n 个 设备 对 m 个 请求 的 响应 。

反之,  若是 固定 1 个线程 “负责” 等待 1 个 请求 的 响应,  则 n 个 请求 须要 n 个线程, 

若是 某设备 的 处理速度 缓慢 或者 故障,  而 对 该设备 的 请求 是 频繁 的,  则 IO 线程 都 会 去等待 这个 设备, 这就 堵塞 了 。

因而 就没有 线程 来 处理 其它 设备 的  IO 了。

这就 回到了 本文 开篇 提出的问题 。

 

经过 上面 3 个 Sequence 图, 咱们能够看到 :

相比同步方法, 就 单次调用 而言, 异步方法 并不会 减小 线程切换 的 次数, 异步方法 的 意义 在于 无阻塞 。

可是 从 整体 来看, 无阻塞 显著 的 减小了 线程 的 数量, 更少 的 线程 意味着 更少 的 切换 。

因此, 从 整体 来看, 异步方法 也是 减小了 线程 切换 次数 的 。

 

无阻塞 是 有利的,   是 计算机软件体系 在 后线程时代 的 一次 发展进化 。

 

无阻塞 还能够用于 SOA ,  好比 SOA 中会有这样的 场景,  一个业务 须要 调用 若干个 服务 来完成 。

这样, 就能够 这样 写代码 :

Foo()

{

         ……     //    一些操做

 

         Task t1 = Service1Async();

         Task t2 = Service2Async();

         Task t3 = Service3Async();

 

         await Task.WhenAll(  { t1,  t2,  t3}  );

 

          ……     //   3 个 服务 都 调用 完成时 要 执行 的 操做

}

 

因为  服务  完成的时间 多是 不肯定 的,  因此 若是 等  服务 1 完成 再 调用 服务 2, 服务 2 完成 再 调用 服务 3, 这样 效率 就比较低 。

因此, 经过 无阻塞 的 方式, 并发调用 多个 服务, 而后 等待 服务 所有 完成, 再作下一步操做, 这样 能够 提升效率 。

固然, 这里的 “等待”,    也是 无阻塞 的 。  ^^

 

在 无阻塞 编程 中, 不能 调用 Thread.Sleep() 来 延时, 这会 阻塞 线程, 占用 线程,

而应该用  await Task.Delay()  方法 来 延时, 或是用 Timer 来设定一个 定时任务, 把 延时后 要作的 工做 放到这个 定时任务 里,

固然, await Task.Delay()  更加的直观, 但 我猜  await Task.Delay()  内部也是用 Timer 原理 实现的 。

而 用 Timer 定时任务 来实现 延时, 这和 Javascript 的 window.setTimeout()  又是 恰如其分 的 类似 。

 

简单的状况, Task t;   t.ContinueWith( 回调 );   能够很好的完成 异步调用 。  Lambda 式 匿名函数 、闭包 以及 Task 的 封装 已经 使 代码 很 简洁直观 。

可是对于一些 场景,  好比 业务系统 三层架构 里 DAL 层 访问数据库, 对数据进行一些处理后 返回 BL 层, BL 层 又把 结果 返回 UI 层,

咱们能够调用 Async 方法 访问数据库, 以实现 无阻塞,  但这种须要对 结果 进行处理 并 层层返回 的 场景, 用 异步回调 的话 代码 就很麻烦,

而 async await  正是 为了 解决  “过多的 异步回调 把 代码 切割的 支离破碎”  的 问题, 因此  async await  是 良性 的 。