协程原理
协程是用户态的轻量级线程,由运行时调度而非操作系统内核。理解协程的实现原理,有助于更好地使用协程编写高并发程序。
协程 vs 线程
线程是内核调度的基本单位,有独立的栈空间(约 1-8MB)、寄存器状态。线程切换需要内核介入,保存和恢复寄存器、栈,切换开销大(约 1-10μs)。
协程是用户态调度的执行单元,有独立的栈空间(约 2-8KB)、寄存器状态。协程切换不需要内核介入,只需要保存和恢复寄存器,切换开销小(约 10-100ns)。
线程可以并行执行(多核),协程只能并发执行(单核)但可以挂起和恢复。协程更适合高并发 I/O 场景,线程更适合 CPU 密集场景。
有栈协程
有栈协程每个协程有独立的栈,可以在任意嵌套调用中挂起。协程切换时保存栈指针、寄存器、程序计数器,恢复时恢复这些状态。
Go goroutine
Go goroutine 是有栈协程,初始栈大小 2KB,按需扩容(最大 1GB)。Goroutine 切换只需要保存三个寄存器:SP(栈指针)、PC(程序计数器)、DX(上下文指针)。Go 运行时将 goroutine 调度到 M(内核线程)上执行。
Lua coroutine
Lua coroutine 也是有栈协程,通过 lua_yield 挂起,lua_resume 恢复。Lua coroutine 的栈大小固定,创建时分配。Lua coroutine 用于协作式多任务,每个任务是一个 coroutine。
无栈协程
无栈协程没有独立的栈,协程状态保存在闭包或状态机中。无栈协程只能在顶层挂起,不能在嵌套调用中挂起。
JavaScript async/await
async 函数返回 Promise,await 等待 Promise 完成。async/await 是 Generator 函数的语法糖,编译器将 async/await 转换成状态机。状态机的状态表示当前执行位置,Promise 完成后继续执行。
Python async/await
Python 3.5 引入 async/await,基于事件循环(asyncio)和 Future。async 函数返回 coroutine 对象,await 等待 Future 完成。asyncio 事件循环运行协程,协程挂起时切换到其他协程。
协程调度
协程调度器决定哪个协程在哪个线程上运行。调度策略影响协程的公平性、响应性和吞吐量。
公平调度
公平调度保证每个协程都有机会执行,避免某个协程饿死。实现方式:时间片轮转,每个协程执行一定时间后切换;优先级调度,高优先级协程优先执行,但低优先级协程也会定期执行。
抢占式调度
抢占式调度允许调度器强制挂起当前协程,切换到其他协程。实现方式:信号抢占(如 Python 的 signal.setitimer)、协作抢占(协程主动让出 CPU,如 Go 的函数调用、通道操作)。
Go 的抢占式调度:函数调用时检查是否需要抢占,如果需要则切换协程。Go 1.14 引入基于信号的抢占,强制挂起执行时间过长的协程。
工作窃取
工作窃取是分布式调度策略,每个线程维护一个本地队列,队列为空时从其他线程的队列窃取任务。Go 的 GMP 调度器采用工作窃取:P(逻辑处理器)维护本地队列,队列为空时从全局队列或其他 P 窃取 G。
协程通信
协程间通信需要特殊机制,因为协程不共享内存(或共享内存需要同步)。
Channel
Channel 是 Go 的协程间通信机制,基于 CSP(Communicating Sequential Processes)模型。Channel 是有类型的管道,可以发送和接收值。Channel 可以是有缓冲的或无缓冲的。无缓冲 Channel 是同步的,发送方和接收方必须同时准备好。
Future/Promise
Future 是对异步结果的封装,Promise 是 Future 的可写版本。Python 的 concurrent.futures.Future,JavaScript 的 Promise。Future 可以等待结果、添加回调、取消任务。
Select
Select 可以等待多个 Channel 操作,哪个先完成就处理哪个。Go 的 select 语句,Python 的 asyncio.select。Select 用于多路复用,等待多个事件。
协程的应用
Web 服务
协程可以处理大量并发连接,每个连接一个协程。Go 的 net/http,每个请求一个 goroutine。Python 的 FastAPI,每个请求一个协程。协程避免了回调地狱,代码像同步代码一样简单。
爬虫
协程可以并发爬取多个页面,每个页面一个协程。协程可以控制并发数量,避免过多连接被服务器拒绝。协程可以等待 I/O 完成后继续执行,不会阻塞其他协程。
数据处理
协程可以并发处理数据,每个数据块一个协程。协程可以流水线处理,一个协程读取数据,一个协程处理数据,一个协程写入数据。Channel 连接流水线的各个阶段。
协程是高并发编程的利器,结合了事件驱动的高并发和多线程的简单性。理解协程的实现原理,有助于更好地使用协程编写高性能程序。