Python协程与异步编程:从生成器到async/await
Python的异步编程经历了从生成器协程到原生async/await的演进过程。理解这条演进路径,才能理解当前异步代码的底层机制。
一、生成器作为协程的基础
Python 2.5引入的yield表达式(而非语句)使生成器具备了双向通信能力。send()方法可以向生成器内部发送值,throw()可以向生成器抛出异常。
python
def simple_coroutine():
x = yield # yield作为表达式接收值
print(f"Received: {x}")
gen = simple_coroutine()
next(gen) # 推进到第一个yield
gen.send(42) # 向yield表达式发送42
生成器在yield处暂停,保存当前栈帧状态:局部变量、程序计数器、异常状态等。send()将值传递给yield表达式,生成器恢复执行。这为协作式多任务提供了基础。
生成器对象内部保存了gi_frame(帧对象)和gi_running状态标志。每次send调用都会恢复帧的执行,直到遇到下一个yield或函数结束。
二、yield from与子生成器委托
yield from是Python 3.3引入的关键特性,它从生成器内部委托给另一个迭代器。
python
def generator1():
for x in range(3):
yield x
def generator2():
yield from generator1() # 委托
yield from不仅仅是语法糖,它建立了双向通道——主调用方通过send发送的值直接传递给子生成器,子生成器产生的值直接返回给主调用方。异常也会在生成器之间传递。
python
def subgen():
v = yield
return v * 2
def delegator():
result = yield from subgen()
yield result
yield from处理了子生成器的return值,并自动传播StopIteration异常(返回值附在异常对象上)。这使得生成器之间可以组成调用链,是协程技术的前身。
三、@asyncio.coroutine与yield from
Python 3.4引入了asyncio模块和@asyncio.coroutine装饰器,使用yield from实现异步等待。
python
@asyncio.coroutine
def fetch_data():
response = yield from aiohttp.request('GET', url)
return response
事件循环管理多个协程对象,当协程等待I/O时调用yield from将控制权返回事件循环,事件循环调度其他就绪协程。协程本身是生成器,yield from暂停当前协程并等待事件完成。
这种方式的限制在于:协程必须用@asyncio.coroutine标记,且必须使用yield from,生成器语义与异步逻辑混在一起,代码可读性较差。
四、async/await:原生协程的引入
Python 3.5引入了async def和await关键字,将协程提升为语言原生概念。
python
async def fetch_data():
response = await aiohttp.request('GET', url)
return response
await与yield from的行为类似,但语义更清晰——await等待的是一个可等待对象,可以是协程对象、Future或实现了__await__的对象。
async def定义的函数不再是生成器,而是原生协程函数,其返回值为协程对象。inspect.isgenerator()返回False,types.coroutine()区分原生协程与生成器协程。
协程对象的底层是PyCoroObject,包含cr_frame、cr_running、cr_await等字段,与生成器的实现相似,但在类型系统层面独立。await会检查对象的__await__方法,如果不存在则触发TypeError。
五、事件循环与Future/Task
事件循环是异步调度的核心。asyncio.get_event_loop()返回当前上下文的事件循环对象。
Future表示异步操作的结果,状态包含PENDING、CANCELLED、FINISHED,set_result()和set_exception()触发回调,等待的协程被唤醒。
Task是Future的子类,负责调度协程的执行。create_task(coro)将协程包装为Task并加入事件循环。事件循环在每次迭代中检查就绪的Future和Task,执行已完成的回调,并恢复等待中的协程。
python
loop = asyncio.get_event_loop() task = loop.create_task(fetch_data()) loop.run_until_complete(task)
run_until_complete启动事件循环,轮询完成,返回Task结果。事件循环的核心是_run_once,它轮询IO多路复用器(select/epoll/kqueue)获取就绪的文件描述符,执行对应的回调,然后恢复等待这些描述符的协程。
六、异步上下文管理器与迭代器
Python 3.7引入了async with和async for。
__aenter__和__aexit__定义了异步上下文管理器,在进入和退出时执行异步操作。
python
async with aiohttp.ClientSession() as session:
response = await session.get(url)
__aiter__和__anext__定义了异步迭代器,async for从中异步获取元素。
python
async for item in async_iter:
process(item)
这些协议让异步代码中也能使用上下文管理和迭代模式。
七、工程实践中的考量
异步编程适合IO密集型场景,将等待时间用于处理其他任务。CPU密集型任务不适合异步,因为计算会阻塞事件循环,run_in_executor将CPU任务交给线程池执行。
协程切换的开销比线程上下文切换低——不涉及操作系统调度,只在用户空间保存和恢复栈帧。
调试异步代码时,asyncio.get_running_loop()确认当前循环存在。PYTHONASYNCIODEBUG环境变量启用调试模式,检查阻塞调用,警告未完成的协程。
异步代码需要从头到尾使用异步库——requests等同步库会阻塞事件循环,aiohttp、httpx异步版本才是正确选择。
八、小结
Python的异步演进路径清晰:生成器 → yield from → @asyncio.coroutine → async/await。每一步都在提升抽象层次,降低异步代码的认知负担。async/await是当前的标准方式,底层仍然是生成器机制和事件循环的协同工作——但语言层将异步与同步代码的语法差异最小化,使异步代码更容易编写和维护。
更多推荐



所有评论(0)