协程 的 主要作用 是 让 单核 GC 变成 单线程 GC

协程 的 主要作用 是 让 单核 GC 变成 单线程 GC,     这样, 在 单核 范围 内,  GC 要 工作 时,  不需要 挂起 线程,   只需要 挂起 协程,

 

因为 协程 的 代码 实际上 是 一个 线程 里 执行的代码,  GC 也是一个 协程,   也 运行在 这个 线程 里,  因此 GC 要 工作 时,挂起协程, 不用 把 寄存器 里 的 对象引用 写回 内存(这个 写回 内存 和 保存 线程 上下文 是 两回事, 线程 切换 时 要 把 寄存器 里 的 数据 保存到内存,这是 保存 上下文,  而 这里 是 把 寄存器 里 的 对象引用 写回 内存 里 对应 的 变量,相当于 一个 内存屏障,  但 理论上 只 写回 引用数据 就行,其它 数据 不关心,  因为 在 线程 架构下, GC 要 扫描 引用,  若 引用 在 寄存器 里 被改写 又 没有 写回 内存,  则 GC 扫描 内存 里 的 引用  就 不对 了),  这样 GC  的 架构 就简单  。    但 协程 架构下的 GC 仍然要 扫描 栈 里 的 局部变量 持有 的 对象引用 。   而  状态机  更进一步,  状态机 可以 在  Step 函数 执行完成 后 执行 GC,  这样 GC 不用 扫描 栈  里 的 局部变量 (持有 的 对象引用) 。   状态机 见 下文  。

 

等等,  纠正一下,  (GC 工作前), 协程 也要 把 寄存器 里的 引用数据 写回 内存,  除非 编译器 预知 切换后 下一个 要 执行 的 协程 是 哪一个,  并 将 下一个 协程 的 代码 内联进来 做 寄存器  优化  (寄存器 布局)。

在 一些 场合,  编译器 可以知道 接下来 要 执行 的 协程 ,  比如 一些 异步操作,  故 可以 把 接下来的 协程 (异步操作) 的 代码 内联 进来,  统一 做 寄存器  优化  (寄存器 布局)。

这样的话,   这些 已经 不是 协程 架构了,   这些 是 状态机 的 内容,  也可以说,  这是 协程 向 状态机 进化 的 苗头  。

但 其实 意义不大,  比如 GC 的 代码 不太可能 和 用户代码 内联 在一起,    通常 用户代码 和 GC 是 两个 独立 的 Step 函数,  既然 是 两个 独立 的 函数,  每个 函数 就是 一个 自然 的 内存屏障  。   既然 是 内存屏障,  当然 函数结束 时 要 把 寄存器 里 的 数据 都 写回 内存  。

这样搞   很晕,  很乱 的  。

 

协程 还是 和 线程 类似 的  “上下文 + 切换” 架构,  当 挂起 协程 时, 要 保存上下文,如果 是 GC 即将工作 要求 挂起的,  还要 把 寄存器  里 的 引用数据 写回 内存,  这和 保存上下文  一定程度 上 是 重复工作  。

状态机 是 彻底 的 函数化,   以 函数 为 执行单位,     不存在 协程 那样 的  “上下文”,   只要 考虑 内存屏障 即可,   同时 编译器 可以 对 函数 统一 做 尽可能深化 的 内联 和  寄存器优化 (寄存器布局)  。

 

 

说到 这里 想起一个 问题,   .Net / C#   async 方法 里 似乎 不能用 lock,  只能用 AsyncLock,  而 Task 里 似乎 是 能 用 lock 的,   Task 里 用 lock 会 怎么样 ?  lock 时 要 挂起 Task 等待 的 话,    是否 采用 协程技术 ?      还是 状态机  ?        而 如果 Task 里 使用  AsyncLock  又会怎么样 ?     感觉 很多 重复技术 嵌套 在 一起,  很乱的,  很费力  。

 

在 完全 的 Task 化 的 情况下,  (Task 化 也 包括 状态机 和 Step 函数) ,   单核 的 GC 架构 可以 变成 最简单 的 单线程 GC,   即 Task 工作结束 后 GC 才工作,  GC 工作时 没有 Task 在 工作,  这样 不用 挂起线程,   也不用 挂起协程,    也不用 扫描 栈 里的 局部变量 (持有 的 对象引用) ,    这样 的 GC 是 最简单 的 形态,   1000 行 代码可以写好,  且 架构 简单清晰,  因为   架构简单清晰,   就 容易做到 安全 稳定  。

 

所以,   小朋友们,   要 让  通用 处理器 和 操作系统   再次 回到  状态机 吗   ? 

 

也许 这个时候,    编译器 就 跳出来 说话了,    这些 不用 改变 通用 处理器 和 操作系统,   所谓 的 “状态机”   可以由 编译器 在 代码 层面  “模拟”  出来  。

 

哈哈,   慢慢 玩 吧  。

 

 

挂起 n 个 协程 比 挂起  n 个 线程 的 开销 低 很多,   最起码,   挂起 线程 需要跟 操作系统 通信,    而 挂起 协程 纯粹 是 一个 线程 内 的 一些 代码 执行  。

 

另外,   因为 n 个 协程 都 只是 一个 线程 内 的 代码 执行,   因此,   可以 对 这些 代码 进行 各种 优化, 就像 优化 普通 的 代码,  一些 函数调用,  包括 内联 和 寄存器优化,   且 可以 减少内存屏障 的 使用次数  。

 

但 想了一下,   协程 不能 减少 内存屏障 的 使用次数,

 

要 深化 内联 和 寄存器优化 的 程度,  减少 内存屏障 使用次数,  应该是 由 一个 状态机 来执行 n 个 任务(协程), 这样 可以 进一步 把 任务(协程) 的 代码 内联 在 状态机 里,  进行 寄存器优化,   比如  .Net / C#   async await,    但 其实 这个技术 很勉强  。

 

 

我在  《从 内存 到 CPU Cache 之间 的 数据读写 的 时间消耗 是 线程切换 性能消耗 的 主要原因 之一 是 不正确 的》   https://www.cnblogs.com/KSongKing/p/14152765.html  里  说 

“所以,    协程  也 不能 搞 太多  。”

“但 现在 看来,    协程 也不能 玩 10 万 个  。”

是说,  协程 不能 解决 线程切换 导致 线程 的 栈 载入载出 Cache 的 性能消耗 的 问题,     协程 的 栈 也有这个问题  。

 

由此 看来,   编译 为 一个 状态机,   代替 n 个 任务(协程),  可以 彻底 取消 n 个 栈,   只要 状态机 一个 栈,  并且 控制 每一个 Step 函数 的 函数调用层级 不要太多,  这样 可以 使 栈  总是 保持 较小  。    当然,   单个 函数 也 不宜 过长,   单个 函数代码 过长,变量过多,  当然 也会 导致 需要 的 栈空间 较大  。

 

所以,   编译器 要 做一个 权衡,  使得,   Step 函数 不要太大,  也不要 太小,   太大 会 占用 较大 的 栈空间,  太小 又 可能 函数调用层级 比较多,  也会 占用 较大 栈空间  。

 

 

前几天 和 之前 讨论 D++ 时候 就 冒出了 本文 的 部分想法,   现在 随想记录一下  。

  

 

以上 内容 写于   2022-04-23 、2022-04-24   。

 

posted on 2022-04-23 04:50  凯特琳  阅读(120)  评论(0编辑  收藏  举报

导航