协程 的 实现方法
可以先看看我之前写的 《再见 异步回调, 再见 Async Await, 10 万 个 协程 的 时代 来 了》 https://www.cnblogs.com/KSongKing/p/10802278.html ,
要实现 协程 , 需要 语言 自己 实现一个 线程池, 然后 把 协程 放到 线程 里 执行 。
协程 切换 就是 把 当前 协程 的 栈顶 和 指令计数器 保存起来 , 在 操作系统 里, 栈顶 和 指令计数器 就是 线程 上下文, 由 操作系统 保存,
对于 协程, 语言 可以 在 堆 里 创建一个 协程 对象, 协程 对象 有 一个 栈顶 字段 和 一个 指令计数器 字段,
协程 的 栈顶 和 指令计数器 就可以保存到 栈顶 字段 和 指令计数器 字段 里,
到 下次 轮到 这个 协程 执行时, 再 把 栈顶 字段 和 指令计数器 字段 的 值 赋值给 寄存器,
这样 就可以 实现 协程 的 切换 了 。
和 操作系统 类似, 语言 需要 维护 一个 协程 的 就绪队列 和 一个 挂起队列 , 协程 在 等待 其它 协程 或者 线程 时, 需要 挂起, 这样 协程 对象 会 添加到 挂起队列, 当 其它 协程 或 线程 完成时, 会 将 协程 从 挂起 队列 移除, 添加 到 就绪队列, 这样 不久的将来 就可以 继续 执行 协程 。
对 就绪队列 和 挂起队列 的 添加 和 移除 操作 是一个 队列 的 操作, 考虑到 并发, 需要在 添加 和 移除 时 Lock(同步 / 互斥), 这可以使用 CAS lock , 而不是 操作系统 提供的 lock, CAS 的 性能 更高, new 操作 应该 就是 使用 CAS lock 来 修改 堆 表, 这样的话, 协程 挂起 和 唤醒 的 时间花费 和 new 操作 差不多 。
总的来说, 协程 切换 的 时间花费 相当于 执行了 一段 很短 的 代码 。
为什么要 语言 自己实现一个 线程池? 而不是 使用 操作系统 提供 的 线程池, 因为 使用 操作系统 提供的 线程池 就成了 async await 了 。 ^^
这又涉及到 协程 和 async await 的 区别 的 问题, 这个问题 和 为什么 要 语言 自己 实现一个 线程池 差不多 是 同一个 问题 。
操作系统 的 线程池 的 任务单位 是一个 函数, 所以, async await 使用 操作系统 的 线程池 的 话, 就需要 把 一个 方法 内 跨线程 调用 前 和 后 的 两部分 代码 切割 为 2 个 函数 , 在 语言 的 层面 , 这 2 个 函数 本身是一个 方法(函数), 所以 函数 2 可以 访问 函数 1 中 的 变量, 但 既然 切割 成了 2 个 函数 , 那么 要 让 函数 2 可以 访问 函数 1 中 的 变量, 就需要 闭包 状态机 等 方式 来 共享 这些 变量 。
对于 协程 , 由于 是 自己 实现 的 线程池 , 所以 把 协程 放在 线程 里 执行 不需要 以 函数 为 单位, 而是 载入 协程 的 上下文(栈顶 指令计数器) 到 寄存器, 然后 继续 执行 指令 就可以, 这些是 语言 在 汇编 层面 完成的, 就是说 是 语言 的 编译器 在 生成 目标代码(汇编)时 将 代码 编译 成 这样的 。
所以, 对于 async await 来说, 把 一个 方法 切割成 函数1 和 函数2 两部分 , 那么 执行 函数1 和 函数2 时 , 两者 有 各自 的 栈顶 , 是 2 次 函数调用,
而 对于 协程, 跨协程 / 跨线程 调用 前 后 的 2 部分 代码 在 执行时 的 栈顶 是 同一个, 是一次 完整的 函数调用 。 只不过 在 跨协程 / 跨线程 调用 时 发生了 “切换” , 即把 寄存器 保存的 栈顶 作为 上下文 保存到 协程 对象 里 , 当 切换回来 时 再 从 协程 对象 的 栈顶 字段 赋值 给 寄存器 , 然后 接着 执行 。
挂起前 和 恢复执行后 的 栈顶 是 同一个, 或者说, 跨协程 / 跨线程 调用 前 后 的 2 部分 代码 的 栈顶 是 同一个, 这 2 部分 代码 是 同一次 函数调用 。
这就是 协程 和 async await 的 本质区别 之一 。
协程 和 async await 的 另一个 本质区别 是 设计理念 的 不同 , async await 是把 异步 变得 看起来 像 同步, 而 协程 是 让 切换 轻量化 、日常化 。
所以, 协程 要 实现的, 是 恢复 传统 的 编程 模型, 同步 是 同步 , 异步 是 异步 , 协程 的 数量 没有 限制, 和 对象 一样, 对象 的 数量 能有多少, 协程 的 数量 就能 有多少, 协程 的 切换 相当于 new 一个对象, 这样 就 能 恢复 传统的 正常的 常规的 编程 模型 范式 架构 思维 思潮 。
这样 就 不需要 异步回调流 为了 节省 线程 就 把 同步 调用 变 异步 , 再 通过 async await 把 异步 代码 变得 “像” 同步代码 。
这 很 糟糕 。
也正因为这样 , 所以 我 说 异步回调流 和 async await 只是一个 过渡, 不可能 长期 霸占 程序界,
而且 , async await 是 怪物 。 哈哈哈哈哈