什么是协程(Coroutine)?它与线程的区别是什么?
协程(Coroutine) 是一种比线程更加轻量级的存在,通常被称为“用户态线程”或“微线程”。
简单来说,协程允许函数在执行过程中暂停(挂起),并在稍后的某个时间点恢复(唤醒),同时保留暂停时的状态(变量、指令指针等)。
为了让你透彻理解,我们从定义、区别、优缺点以及应用场景几个方面来详细拆解。
1. 什么是协程?
在传统的编程中,函数调用是层级式的:A 调用 B,B 执行完返回 A,A 继续执行。
而在协程中,调用是协作式的:A 可以执行一半把控制权交给 B,B 执行一半又交回给 A,如此往复。
生活类比:
- 普通函数(单线程): 你在看书(任务A)。突然电话响了,你必须把书读完(或者读完这一章)才能去接电话。
- 多线程: 你雇了一个人帮你接电话。你看书,他接电话。但是你们俩共用一个脑子(CPU),操作系统强制你们每隔几毫秒轮流使用脑子,切换过程很累。
- 协程: 你在看书(任务A)。电话响了,你拿个书签夹住当前页(保存状态),放下书去接电话(任务B)。电话挂了,你拿起书翻到书签页继续看(恢复状态)。这一切都是你自己控制的,没有外人(操作系统)强迫你切换。
2. 协程与线程的区别
这是面试或技术讨论中最核心的部分。我们可以从以下几个维度对比:
| 维度 | 线程 (Thread) | 协程 (Coroutine) |
|---|---|---|
| 管理主体 | 操作系统内核 (Kernel) | 用户态程序 (程序员/运行时环境) |
| 调度机制 | 抢占式 (Preemptive)。操作系统决定何时切换,线程自己无法控制。 | 协作式 (Cooperative)。协程主动让出 CPU 控制权 (yield/await)。 |
| 资源消耗 | 重。每个线程需要独立的栈空间(通常 MB 级别),且占用内核资源。 | 轻。栈空间极小(KB 级别),甚至可以动态伸缩。 |
| 切换开销 | 高。涉及内核态与用户态的切换,需要保存寄存器、刷新缓存等。 | 极低。只涉及用户态的寄存器状态保存,像函数调用一样快。 |
| 并发能力 | 受限于内存和内核调度,单机几千个线程可能就到瓶颈了。 | 单机可以轻松运行几十万甚至上百万个协程。 |
| 同步机制 | 需要复杂的锁(Mutex)、信号量来防止数据竞争。 | 在单线程运行协程的情况下,不需要锁(因为不会同时修改数据),只需判断状态。 |
核心差异总结:
- 线程是操作系统层面的并发,由 OS 负责调度,适合利用多核 CPU 实现并行。
- 协程是语言/应用层面的并发,由代码负责调度,适合处理高并发的 I/O 任务。
3. 为什么我们需要协程?(协程解决了什么问题)
既然有了线程,为什么还要发明协程?主要为了解决 I/O 密集型任务 的效率问题。
场景:网络爬虫或 Web 服务器
假设你要下载 100 张图片。
- 单线程同步: 下载第 1 张 -> 等待网络 -> 下载完 -> 下载第 2 张...(CPU 大部分时间在傻等网络响应,效率极低)。
- 多线程: 开 100 个线程。虽然不用傻等,但 100 个线程的创建、销毁和上下文切换消耗了大量 CPU 资源,内存也可能爆掉。
- 协程:
- 发起第 1 张下载请求,不等待,直接挂起当前协程。
- 利用等待网络的空闲时间,发起第 2 张下载请求...
- 一旦第 1 张图片的数据回来了,事件循环(Event Loop)会唤醒第 1 个协程继续处理。
- 结果: 只需要一个线程,几乎没有切换开销,CPU 始终在工作,没有浪费在等待上。
4. 常见的协程实现
不同的编程语言对协程的支持方式不同:
Python (async/await):
Python 3.5+ 引入了原生协程。pythonimport asyncio async def task(): print("开始") await asyncio.sleep(1) # 模拟IO操作,主动让出控制权 print("结束") # 可以在一个线程里并发运行成千上万个这样的 taskGo (Goroutine):
Go 语言的杀手级特性。虽然叫协程,但它是一种 M:N 模型(M个协程映射到 N个系统线程上)。- 它既有协程的轻量(几 KB 内存),又能利用多核 CPU(因为底层会有多个线程在跑)。
- 使用极简:
go functionName()。
Kotlin / Java (Project Loom/Virtual Threads):
Kotlin 有原生协程支持。Java 在 JDK 21 推出了虚拟线程(Virtual Threads),本质上也是协程的一种实现,旨在让高并发编程像写传统阻塞代码一样简单。Lua:
Lua 是最早支持协程的脚本语言之一,很多游戏(如《魔兽世界》)利用 Lua 协程来控制游戏逻辑脚本。
5. 总结
- 协程就是用户态的、轻量级的、可暂停和恢复的函数。
- 区别在于:线程由操作系统调度(抢占式,开销大),协程由程序自己调度(协作式,开销小)。
- 适用场景:协程非常适合 I/O 密集型 任务(如网络请求、数据库查询、文件读写),能在单线程下实现极高的并发。对于 CPU 密集型 任务(如视频解码、复杂计算),协程优势不大,通常还是需要多进程或多线程来利用多核 CPU。