无阻塞 编程模型 涉及到 异步回调流, 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 是 良性 的 。