协程(coroutine)与生成器(generator)的底层实现有何异同?
协程(coroutine)与生成器(generator)在 Python 中看似相似(均使用 yield
或 await
暂停执行),但底层实现和设计目标存在显著差异。以下从执行机制、内存管理和应用场景三个维度解析其异同:
1. 底层实现的演变与核心差异
(1) 生成器(Generator)
- 本质:通过
yield
关键字实现的惰性迭代器,用于按需生成数据。 - 实现机制:
- 生成器函数被调用时,返回一个生成器对象(类型为
generator
)。 - 生成器对象内部维护一个栈帧(frame),保存局部变量和执行位置(
f_lasti
指针)。 - 每次调用
next()
或send()
时,恢复执行到yield
处,并挂起当前状态。
- 生成器函数被调用时,返回一个生成器对象(类型为
- 底层数据结构:
# 生成器对象的 C 结构(CPython 源码示例) typedef struct { PyObject_HEAD PyFrameObject *gi_frame; # 当前执行栈帧 PyObject *gi_code; # 字节码对象 PyObject *gi_weakreflist; # 弱引用列表 // ... } PyGenObject;
(2) 协程(Coroutine)
- 本质:通过
async/await
实现的异步任务调度单元,用于非阻塞并发。 - 实现机制:
- 协程函数(
async def
)被调用时,返回一个协程对象(类型为coroutine
)。 - 协程对象依赖**事件循环(Event Loop)**调度,通过
await
挂起并让出控制权。 - 底层基于**生成器协程(Python 3.4 的
@asyncio.coroutine
)**演化,但优化了异步语义。
- 协程函数(
- 底层数据结构:
# 协程对象的 C 结构(CPython 源码示例) typedef struct { PyObject_HEAD PyObject *cr_origin; # 协程创建位置(调试用) PyObject *cr_frame; # 协程栈帧 PyObject *cr_code; # 字节码对象 // ... } PyCoroObject;
2. 关键异同对比
特性 | 生成器(Generator) | 协程(Coroutine) |
---|---|---|
设计目标 | 惰性数据生成 | 异步并发与协作式多任务 |
暂停/恢复机制 | yield 暂停,外部 next() 恢复 | await 暂停,事件循环调度恢复 |
对象类型 | generator | coroutine 或 async_generator |
执行驱动方 | 外部调用者(手动迭代) | 事件循环(自动调度) |
状态管理 | 仅维护栈帧和局部变量 | 额外维护任务状态(如 Future 对象) |
异常传播 | 通过 throw() 注入异常 | 由事件循环统一处理异常 |
内存消耗 | 轻量(单次迭代状态) | 较重(需维护任务链和回调) |
典型应用场景 | 大数据流处理、惰性计算 | 高并发I/O操作(如网络请求、文件读写) |
3. 执行流程的底层差异
(1) 生成器的挂起与恢复
def gen():
yield 1
yield 2
g = gen()
print(next(g)) # 输出1
print(next(g)) # 输出2
- 底层操作:
- 调用
gen()
创建生成器对象,状态为GEN_CREATED
。 next(g)
触发gi_frame
执行,直到遇到yield
,状态变为GEN_SUSPENDED
。- 再次调用
next(g)
恢复gi_frame
执行。
- 调用
(2) 协程的调度与执行
async def coro():
await asyncio.sleep(1)
print("Done")
async def main():
await coro()
asyncio.run(main())
- 底层操作:
asyncio.run()
创建事件循环,调度main()
协程。await coro()
挂起main()
,将控制权交还事件循环。- 事件循环监控
asyncio.sleep(1)
的完成状态,1秒后恢复coro()
执行。
4. 性能优化与实现细节
- 生成器的局限性:
- 无法直接嵌套
yield
和yield from
以外的异步操作。 - 手动管理迭代流程,难以实现高并发。
- 无法直接嵌套
- 协程的优化:
async/await
语法糖:将协程与生成器解耦,避免语义混淆。Task
对象封装:协程被包装为Task
,由事件循环统一调度。- 零拷贝挂起:通过
await
直接切换协程,减少上下文切换开销。
5. 从生成器到协程的演化
- Python 3.4:通过
@asyncio.coroutine
和yield from
实现协程(基于生成器)。 - Python 3.5:引入原生协程(
async def
)和await
关键字,与生成器分离。 - Python 3.7:
async
和await
成为正式关键字,协程性能进一步优化。
总结
- 相同点:均通过暂停/恢复机制实现非阻塞执行,依赖状态保存与恢复。
- 不同点:
- 生成器是同步的、迭代驱动的,设计目标为数据生成;
- 协程是异步的、事件循环驱动的,设计目标为高并发任务调度。
- 选择建议:
- 需要惰性生成数据 → 使用生成器;
- 需高并发处理I/O密集型任务 → 使用协程(配合
asyncio
)。