再见 异步回调, 再见 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 语法糖 上 纠缠下去 像是一个 泥塘 , 新一代 的 架构 是 重新 整理 线程 这个 东西 , 在 语言 级别 实现 语言级 的 “线程”(协程) 是一个 最好 的 平衡点 。