Python 协程与生成器原理详解:从暂停恢复到异步编程
在 Python 的世界里,协程(coroutine)和生成器(generator)是两个非常重要的概念。它们都涉及“暂停和恢复”的机制,但它们的设计目标、使用场景以及实现细节却大相径庭。
1. 生成器:数据流的生产者
1.1 什么是生成器?
生成器是一种特殊的迭代器,它的核心功能是通过 yield 暂停和恢复执行。生成器的设计初衷是为了按需生成数据流,避免一次性加载所有数据到内存中导致内存溢出。
def my_generator():
yield 1
yield 2
yield 3
gen = my_generator()
for value in gen:
print(value)
在这个例子中:
-
调用
my_generator()时,它不会立即执行函数体中的代码,而是返回一个生成器对象。 -
每次调用
next(gen)或使用for循环时,生成器会从上次暂停的地方继续执行,直到遇到下一个yield。
生成器的状态保存机制(依赖于 Python 的栈帧机制)使得它能够暂停和恢复执行,但这种机制并不涉及任何调度逻辑。
1.2 生成器的驱动方式
生成器的执行是由调用者驱动的,通常通过以下方式:
-
调用
next()方法驱动生成器的执行。 -
使用
for循环自动调用next()。
生成器的数据流动是单向的:生成器通过 yield 向调用者发送数据。
除了通过 yield 发送数据外,生成器还支持来自调用方的反向通信。调用者可以通过 send() 方法向生成器发送数据,或者通过 throw() 方法向生成器抛出异常。
def coroutine():
while True:
value = yield
print(f"Received: {value}")
c = coroutine()
next(c) # 启动生成器
c.send(10) # 输出 "Received: 10"
c.send(20) # 输出 "Received: 20"
在这个例子中:
-
yield暂停生成器的执行。 -
send()将值发送到生成器中,并恢复生成器的执行。
1.3 生成器的底层实现
生成器对象是一个实现了 __iter__ 和 __next__ 方法的对象。当你定义一个包含 yield 的函数时,Python 会将其标记为生成器函数,并返回一个生成器对象。
def my_generator():
yield 1
gen = my_generator() # 返回一个生成器对象
print(gen.__next__) # 生成器对象实现了 __next__ 方法
2. 协程:异步编程的基石
2.1 什么是协程?
协程是一种可以暂停和恢复执行的函数,主要用于处理并发任务和异步操作。协程的设计目标是通过事件循环调度多个任务,从而在单线程中实现高效的并发。
import asyncio
async def task():
print("Before sleep")
await asyncio.sleep(1) # 暂停协程
print("After sleep")
asyncio.run(task())
在这个例子中:
-
当协程遇到
await asyncio.sleep(1)时,它会暂停并将控制权交还给事件循环。 -
事件循环会在计时器到期后恢复协程的执行。
2.2 事件循环的作用
事件循环是异步编程的核心组件,负责调度和管理协程的执行。它的主要职责包括:
-
调度任务 :决定何时执行哪个协程。
-
监控 I/O 状态 :使用 I/O 多路复用机制(如
epoll、select)监控文件描述符的状态变化。 -
处理定时器 :管理计时器任务,并在计时器到期时恢复对应的协程。
-
执行回调函数 :在适当的时候调用注册的回调函数。
事件循环使得多个协程可以在单线程中高效地并发执行。
2.2.1 定时器队列
事件循环内部维护了一个定时器队列 (timer queue),用于存储所有注册的定时器任务。每个定时器任务包含以下信息:
-
到期时间 :计时器到期的时间点。
-
回调函数 :计时器到期时需要执行的操作(如恢复协程)。
定时器队列通常按照到期时间排序,这样事件循环可以快速找到最早到期的计时器。
2.2.2 I/O 多路复用机制
大多数 I/O 多路复用机制(如 select、poll、epoll、kqueue 等)支持设置超时时间。事件循环会利用这些机制的超时功能来等待计时器到期。
例如,在 Linux 上使用 epoll 时:
-
事件循环会调用
epoll_wait(),并传入一个超时时间(通常是下一个计时器到期的时间)。 -
如果在超时时间内没有其他 I/O 事件发生,
epoll_wait()会在超时后返回,此时事件循环会检查是否有计时器到期。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
这里的 timeout 参数就是事件循环用来等待计时器到期的时间。
2.2 事件循环的工作流程
事件循环的工作流程大致如下:
-
注册任务 :将协程注册到事件循环中。
-
调度任务 :事件循环根据协程的状态决定何时执行它。
-
暂停和恢复 :当协程遇到
await时,它会暂停并将控制权交还给事件循环。事件循环会去执行其他任务,直到await后的操作完成。
2.3 协程的运行时栈管理
协程的暂停/恢复机制依赖 Python 解释器的栈帧管理。与传统线程不同,协程的栈空间存储在堆内存中,这使得每个协程仅需约 3KB 内存开销。当协程执行 await 时:
-
当前栈帧被冻结并存入堆内存
-
事件循环记录恢复点(通过
__await__方法) -
控制权返回事件循环,执行其他任务
这种机制使得 Python 单进程可轻松支持数万个并发协程,远超线程的并发能力(通常受限于操作系统的线程数限制)。
2.4 异步上下文切换的性能优势
| 对比项 | 线程切换 | 协程切换 |
|---|---|---|
| 切换耗时 | 1-5μs | 80-150ns |
| 切换触发源 | 操作系统调度 | 显式 await 点 |
| 内存开销 | MB 级栈空间 | KB 级堆存储 |
这种性能差异源于协程切换完全在用户态完成,无需陷入内核态。实测表明,协程密集型的 HTTP 服务可达到 10k+ QPS,而线程池方案通常在 2k QPS 左右即出现明显性能衰减。
2.5 协程与其他并发模型的对比
在 Python 生态中,协程、线程、进程分别适用于不同场景:
| 模型 | 最佳场景 | 优点 | 缺点 |
|---|---|---|---|
| 协程 | I/O 密集型高并发 | 低内存开销、高吞吐量 | 无法并行执行CPU任务 |
| 多线程 | 简单并发、阻塞I/O | 编程模型简单 | GIL限制、切换开销大 |
| 多进程 | CPU密集型任务 | 绕过GIL、利用多核 | 内存隔离、启动延迟高 |
混合使用示例(CPU密集型+异步I/O):
async def hybrid_work():
loop = asyncio.get_running_loop()
# 将CPU任务提交到进程池
await loop.run_in_executor(
ProcessPoolExecutor(),
cpu_intensive_function
)
# 异步处理I/O
await db_query()
该模式结合了进程池的并行计算能力和协程的高效I/O处理,特别适用于需要同时处理计算和网络请求的复杂场景。
3. 生成器与协程的区别
尽管生成器和协程都支持“暂停和恢复”,但它们的设计目标和使用场景完全不同。
3.1 设计目标不同
-
生成器 :主要用于生成数据流或迭代器。
-
协程 :主要用于处理并发任务和异步操作。
3.2 并发需求不同
-
生成器 :生成器通常是串行执行的,不需要并发。
-
协程 :协程通常需要处理多个任务的并发执行,因此需要事件循环来调度。
| 特性 | 生成器 | 协程 |
|---|---|---|
| 设计目标 | 专注数据流的惰性生成 | 处理并发任务,专注异步任务的高效调度 |
| 数据流动方向 | 单向(生成器向调用者发送数据) | 双向(协程可以接收外部传入的数据) |
| 是否需要事件循环 | 不需要 | 需要 |
| 适用场景 | 数据流生成、逐行读取大文件等 | 网络请求、定时器、文件 I/O 等 |
生成器和协程虽然在技术上有相似之处,但它们的设计目标和应用场景完全不同。生成器专注于生成数据流,而协程专注于异步编程。随着 Python 在异步编程领域的不断发展,async/await 已经成为现代异步编程的标准工具。
4. 从生成器到协程
4.1 协程的起源:基于生成器
生成器的核心特性是能够通过 yield 暂停函数的执行,并在稍后恢复执行。这种“暂停和恢复”的机制与协程的需求非常相似,因此生成器被用作早期协程的基础。
在 Python 3.4 之前,协程的概念并没有专门的语法支持,而是通过生成器实现的。生成器通过 yield 和 send() 实现了简单的协程行为。
def coroutine():
while True:
value = yield
print(f"Received: {value}")
c = coroutine()
next(c) # 启动协程
c.send(10) # 输出 "Received: 10"
c.send(20) # 输出 "Received: 20"
这种方式虽然可以模拟协程的行为,但它并不是专门为异步编程设计的。
4.2 生成器的局限性
尽管生成器可以模拟协程的行为,但它并不适合直接用于异步编程,主要原因包括:
-
语义模糊 :生成器的主要目的是生成数据流,而协程的主要目的是处理异步任务。使用生成器来模拟协程会导致语义上的混淆。
-
缺乏原生支持 :生成器本身并没有内置的异步支持。例如,生成器无法直接与事件循环集成,也无法轻松处理复杂的异步任务。
-
错误处理困难 :生成器的错误处理机制(如
StopIteration异常)与协程的需求不匹配,容易导致代码复杂化。 -
性能问题 :生成器的实现方式虽然简单,但在处理大量并发任务时性能较差,尤其是在需要频繁切换任务的情况下。
4.3 Python 3.3:yield from 的引入
为了简化生成器之间的委托关系,Python 3.3 引入了 yield from 语法。这为协程的实现提供了更强大的工具。
def sub_generator():
yield 1
yield 2
def delegating_generator():
yield from sub_generator()
for value in delegating_generator():
print(value) # 输出 1 和 2
yield from 不仅可以用于生成器,还可以用于协程之间的任务委托。这使得生成器可以更好地支持复杂的任务链式调用。
尽管 yield from 提高了生成器的灵活性,但它仍然无法解决生成器在异步编程中的根本问题。
4.4 Python 3.4:asyncio 的引入
为了更好地支持异步编程,Python 3.4 引入了 asyncio 库,这是 Python 第一个官方支持的异步框架。asyncio 提供了一个事件循环(event loop),用于调度和管理协程的执行。
import asyncio
@asyncio.coroutine
def fetch_data():
yield from asyncio.sleep(1)
return "Data"
@asyncio.coroutine
def main():
result = yield from fetch_data()
print(result)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
在这个例子中:
-
@asyncio.coroutine装饰器将普通函数标记为协程。 -
yield from用于等待另一个协程或异步操作完成。 -
事件循环负责调度协程的执行。
虽然 asyncio 提供了对异步编程的初步支持,但它的语法仍然依赖于生成器,显得不够直观。
4.5 Python 3.5:async/await 的诞生
为了进一步简化异步编程,Python 3.5 引入了 async/await 语法。这是对生成器机制的高级封装,提供了更清晰、更高效的异步编程方式。
import asyncio
async def fetch_data():
await asyncio.sleep(1)
return "Data"
async def main():
result = await fetch_data()
print(result)
asyncio.run(main())
在这个例子中:
-
fetch_data是一个协程函数。 -
await asyncio.sleep(1)暂停fetch_data协程的执行,等待 1 秒钟。 -
事件循环负责调度协程的执行。
async/await 的引入解决了生成器在异步编程中的诸多问题:
-
语义明确 :
async/await明确表示这是一个异步操作,而不是普通的生成器。 -
类型区分 :协程对象与生成器对象在类型上是不同的。例如,
inspect.iscoroutine()可以用来检查一个对象是否是协程。 -
内置支持 :
async/await提供了对异步编程的原生支持,包括事件循环、任务调度等。
async/await 提供了更清晰、更高效的异步编程方式,解决了生成器在异步编程中的局限性。
4.6 Python 3.6 及以后:进一步优化
从 Python 3.6 开始,async/await 语法得到了进一步优化,支持更多高级功能,例如异步生成器和异步上下文管理器。
4.6.1 异步生成器
异步生成器允许你在生成器中使用 await,从而实现按需生成异步数据流。
import asyncio
async def async_generator():
for i in range(3):
await asyncio.sleep(1)
yield i
async def main():
async for value in async_generator():
print(value)
asyncio.run(main())
4.6.2 异步上下文管理器
异步上下文管理器允许你在 async with 语句中使用异步资源。
import asyncio
class AsyncContextManager:
async def __aenter__(self):
print("Entering context")
await asyncio.sleep(1)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
print("Exiting context")
await asyncio.sleep(1)
async def main():
async with AsyncContextManager():
print("Inside context")
asyncio.run(main())
这些新特性进一步增强了 async/await 的功能,使其成为现代 Python 异步编程的标准工具。
从生成器到协程的演变过程可以总结为以下几个关键阶段:
-
生成器的暂停与恢复 :生成器通过
yield实现了简单的暂停和恢复机制,奠定了协程的基础。 -
yield from的引入 :yield from简化了生成器之间的任务委托,提高了生成器的灵活性。 -
asyncio的诞生 :asyncio提供了对异步编程的初步支持,但仍然依赖于生成器。 -
async/await的引入 :async/await是对生成器机制的高级封装,提供了更清晰、更高效的异步编程方式。 -
进一步优化 :从 Python 3.6 开始,
async/await支持更多高级功能,如异步生成器和异步上下文管理器。
生成器专注数据流的惰性生成。二者均利用暂停/恢复机制,但协程通过事件循环实现多任务并发,是 Python 异步编程的核心。

浙公网安备 33010602011771号