再见 异步回调, 再见 Async Await, 10 万 个 协程 的 时代 来 了

有关 协程 原理,  见 《协程 和 async await》   https://www.cnblogs.com/KSongKing/p/10799875.html  ,

 

协程 切换 的时间很快,   就是 保存 3 个 寄存器 的 值, 再 修改 3 个 寄存器 的 值,

这  3 个 寄存器 分别 保存的 是  协程 的 栈顶 栈底 指令计数器,

 

切换 协程 就是  保存 上一个 协程 的  栈顶 栈底 指令计数器 ,   再 把 寄存器 的 值 修改 为 下一个 协程 的 栈顶 栈底 指令计数器  。

 

所以,  协程 切换 相当于 是 调用一个 很短 的  方法,  时间复杂度 可以认为是  O(1)    。

 

我在  《协程 和 async await》   中 提到, 协程 避免 闭包 共享变量 带来的 二次寻址 的 性能消耗,

 

但是,实际上,    有了 协程  的 话,    已经不需要 异步回调 来   减少 线程数量 和 线程切换时间,  当然 也不需要  async await  。

 

我们可以 正常 的 编写 同步调用 的 方法,比如,  读取文件, 可以调用 FileStream 的 Read()   方法,  而 不需要 调用 ReadAsync() 或者 BeginRead() 方法 。

 

当然,这个 Read()  方法 需要用  协程 重新实现,   Read()  方法 的 同步等待 要用 协程 的 同步等待 来实现,而不是 线程 的 同步等待 。

 

协程 的 同步等待 和 线程 一样,   在 Read() 方法 里,   当 调用 操作系统 的 异步 IO   API 时,   当前 协程 挂起,  当前 线程 转而 执行其它 协程 ,

当 异步 IO 完成时,会把  调用这个 IO 的 挂起 的 协程 唤醒(放入 就绪队列),这样 接下来 很快就会 执行到 这个 协程  了  。

 

这和 线程 是一样的  。    但 我就不知道 操作系统 到底 做了些什么,   以至于 线程 这个 “资源”  如此 “昂贵”,  搞得 这些年 流行 炒作 线程,   像  炒股 一样 炒 的 很热,

什么    异步回调 流, libuv, IOCP , ePoll ,  async await ,  状态机,  编译器 黑魔法,   GoRoutine,  CoRoutine     ……

Go  语言 的  介绍    每次都 强调 “支持高并发”, “编写服务器端程序”  ,       我原来还奇怪,  高并发 这个不是 很平常 么,  C# 不也是 高并发 没毛病  ……

 

这几天 才 想起来,  原来  Go   强调 “支持 高并发”   大概 是  因为 有  GoRoutine  啊   ~~  !

 

这些 基于 异步回调  和  语法糖   的 做法,   将 代码  切割 的 支离破碎,    语法糖  篡改了  原始代码 ,  让 编译器 变得 笨重复杂,  让  程序员 的 代码 和 编译器 的 代码 变得 难以理解 ,      让 编译器 技术    升维  为   大型 软件工程   。

 

我在 《协程 和 async await》  中 提到,  协程 需要 编译器 在 堆 里 模拟 线程 的  栈 和 上下文 保存区,   其实还需要 模拟一样,  就是 操作系统(CPU) 的   时间片 调度,        这可有点难,  弄不好 性能 就 一落千尺,  与 我们 轻量 高性能 的 目标 背道而驰  。

 

但是,  实际上,  协程 基本上 不需要 时间片 调度,    只需要 不停执行 就可以,   只有 遇到 IO 的 时候 才 挂起, 执行下一个 协程,  就可以了   。

 

这样就 差不多 了  。

 

在 协程 架构下,  对于 每一个 请求 ,  可以 创建一个 协程 来处理,处理完了 协程 就 销毁,  可以 存在 大量  协程  ,

这 体现 出 与 线程 的 区别 是,   不需要 考虑  创建开销,  不需要 考虑  切换开销,  不需要 考虑(协程) 数量  。

 

在 一台 服务器 上,    同时 运行  10 万 个 协程 也可以   。

 

这样就可以和        异步回调  ,   Async  Await        说 再见 了     。

 

这样 就可以  “回归最柔软的初心, 诗意的栖居在大地上”    ,     什么是   “柔软的初心”     ?

 

就是  80 年代 的 苹果电脑 、 中华学习机 、 286 、 Basic 、  90 年代  的  谭浩强 爷爷 的 C 语言   。

还有  70 年代 的 Unix  、  贝尔实验室  、   网络链路 和 网路协议   。

 

协程   是   ILBC / D#    的  一大 卖点    。

 

大家可能 担心  对 协程 的 就绪队列 和 挂起队列 的 队列 读写 需要 Lock(同步互斥),  这个 Lock  的 性能 怎么样,

我之前在   ILBC / D#     中 提到过   ILBC  用 CAS 指令 实现  IL Lock ,  用于  堆 管理(读写 堆  表),

这个做法 和 C# Java 的 new 操作  读写堆表 时 用的 做法 应该是一样的(我推测)  。

CAS  实现 的 lock 是 很快 的,  可以认为 是 指令级 的 ,   可以认为 时间复杂度 是 O(1)  。

事实上,  测试  C#  的 new 操作 的 性能 大约 是  操作系统 lock (lock 关键字 / Monitor.Enter()) 的  10  倍  。

或者说,  C#  的 new 操作 的 时间花费 大约 是  操作系统 lock (lock 关键字 / Monitor.Enter()) 的  1/10    。

 

在  “异步回调”  和  async await 语法糖  上  纠缠下去 像是一个 泥塘 ,   新一代 的 架构 是 重新 整理 线程 这个 东西 ,     在 语言 级别 实现 语言级 的 “线程”(协程) 是一个 最好 的 平衡点    。

 

 

 

 

 

 

posted on 2019-05-02 14:28  凯特琳  阅读(1187)  评论(0编辑  收藏  举报

导航