协程概念与编程模型
协程概念与编程模型
协程(Coroutine)是一种用户态的轻量级并发执行单元。和操作系统线程不同,协程的调度由程序自己控制,避免了内核态切换的开销。
为什么需要协程
在高并发场景中,线程有几个明显的瓶颈:
- 上下文切换成本高:线程切换需要陷入内核态,保存/恢复寄存器、刷新 TLB
- 内存开销大:每个线程默认栈空间 1~8MB,1 万个线程就是 GB 级内存
- 数量有限:操作系统能管理的线程数是有限的
协程的本质思路是把"并发"和"并行"解耦——用少量线程承载大量协程,协程之间通过协作式调度(非抢占式)切换执行。
简单来说:线程是操作系统调度的,协程是程序员/运行时调度的。
协程的两种实现方式
有栈协程(Stackful Coroutine)
每个协程有自己的独立调用栈,可以在任意位置挂起和恢复。
Thread Thread
┌─────────┐ ┌─────────┐
│ Coroutine A │ Coroutine B
│ ├─ stack │ ├─ stack
│ └─ context │ └─ context
└─────────┘ └─────────┘代表:Go 的 goroutine、Lua 的协程。优点是可以像写同步代码一样写异步,缺点是每个协程需要预分配栈空间。
无栈协程(Stackless Coroutine)
协程不拥有独立栈,状态保存在堆上的状态机中,由编译器/运行时的 async/await 关键字驱动。
代表:Python 的 asyncio、Rust 的 Future、JavaScript 的 Promise/async-await。优点是内存开销极小(通常几十字节),缺点是函数色传染(async 函数只能被 async 函数调用)。
| 特性 | 有栈协程 | 无栈协程 |
|---|---|---|
| 栈管理 | 独立栈空间 | 编译器状态机 |
| 内存开销 | KB 级 | 几十字节 |
| 挂起点 | 任意位置 | 仅在 await 点 |
| 函数色 | 无 | 有(async 传染) |
| 代表语言 | Go, Lua | Python, Rust, JS |
各语言的协程实现
Python — asyncio
import asyncio
async def fetch_data(url):
print(f"Fetching {url}...")
await asyncio.sleep(1) # 模拟网络 IO
print(f"Done: {url}")
return f"data from {url}"
async def main():
tasks = [fetch_data(f"url_{i}") for i in range(3)]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())Python 的协程基于事件循环(event loop)。await asyncio.sleep(1) 会将控制权交还给事件循环,让其他协程有机会执行。注意 IO 操作才需要 await,CPU 密集型操作会阻塞整个事件循环。
Rust — async/await + Future
use tokio;
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
"hello from async task"
});
let result = handle.await.unwrap();
println!("{}", result);
}Rust 的 async 是编译时转换为状态机的零成本抽象。Future 通过 poll 方法驱动,直到返回 Poll::Ready。
与 Python 最大的区别:Rust 的 Future 是惰性的——不调用 .await 不会执行任何代码,需要显式交给 executor。
Go — goroutine
package main
import (
"fmt"
"time"
)
func main() {
go func() {
time.Sleep(1 * time.Second)
fmt.Println("goroutine done")
}()
fmt.Println("main continues")
time.Sleep(2 * time.Second)
}Go 的 goroutine 是有栈协程,由 Go runtime 调度(G-P-M 模型)。go 关键字创建一个新的 goroutine,runtime 自动在系统线程上多路复用。
Go 的优势是函数色不存在——任何函数都可以通过 go 关键字异步执行。代价是 goroutine 栈会动态增长,起始 2KB,比无栈协程大不少。
协程 vs 线程 vs 事件驱动
| 维度 | 线程 | 协程 | 事件驱动(回调) |
|---|---|---|---|
| 调度方 | OS 内核 | 用户态运行时 | 用户态事件循环 |
| 切换成本 | 高(1~10μs) | 低(~ns 级) | 低 |
| 内存开销 | MB 级 | KB/Byte 级 | 低 |
| 编程模型 | 同步 | 同步(await) | 异步(回调地狱) |
| 并行能力 | 真并行 | 并发非并行 | 并发非并行 |
总结
协程通过用户态调度实现了轻量级并发。有栈协程(Go)用起来更自然但内存开销更高;无栈协程(Rust/Python/JS)更轻量但需要 async/await 语法配合。
理解协程的关键在于理解"协作式调度"——协程主动让出控制权,而不是被操作系统抢占。这意味着如果一个协程不主动 await,它会一直独占线程,其他协程就得等着。