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.coroutineyield 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 defawait关键字,将协程提升为语言原生概念。

python

async def fetch_data():
    response = await aiohttp.request('GET', url)
    return response

awaityield from的行为类似,但语义更清晰——await等待的是一个可等待对象,可以是协程对象、Future或实现了__await__的对象。

async def定义的函数不再是生成器,而是原生协程函数,其返回值为协程对象。inspect.isgenerator()返回Falsetypes.coroutine()区分原生协程与生成器协程。

协程对象的底层是PyCoroObject,包含cr_framecr_runningcr_await等字段,与生成器的实现相似,但在类型系统层面独立。await会检查对象的__await__方法,如果不存在则触发TypeError

五、事件循环与Future/Task

事件循环是异步调度的核心。asyncio.get_event_loop()返回当前上下文的事件循环对象。

Future表示异步操作的结果,状态包含PENDINGCANCELLEDFINISHEDset_result()set_exception()触发回调,等待的协程被唤醒。

TaskFuture的子类,负责调度协程的执行。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 withasync 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等同步库会阻塞事件循环,aiohttphttpx异步版本才是正确选择。

八、小结

Python的异步演进路径清晰:生成器 → yield from → @asyncio.coroutine → async/await。每一步都在提升抽象层次,降低异步代码的认知负担。async/await是当前的标准方式,底层仍然是生成器机制和事件循环的协同工作——但语言层将异步与同步代码的语法差异最小化,使异步代码更容易编写和维护。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐