Python 协程进化史:从 yield 到 async/await 的底层实现

第一次真正对python的协程产生兴趣,不是有着一颗善于发现问题的眼睛和好学的心,而是在一次钱少 事多 程序烂 的私活里。

那时候我写了一个小爬虫,逻辑很简单:请求页面、解析数据、存库。代码本身没什么难度,真正让人难受的是待处理数据非常非常多的情况下。等网络返回,等接口响应,等磁盘写入,等各种本来不该占用 CPU 的时间。最开始我想得很天真,既然一个任务在等,那就多开几个线程一起跑,总能快一点吧。

结果确实快了,但快得很无语,得亏本帅是游戏本扛得住。线程一多,内存开始涨,调度开始乱,问题也跟着冒出来。程序像是突然从能用变成了能跑,但不太像人写的。其实很多性能问题不是算力不够,而是等待太多、切换太重、资源浪费得太明显。

flowchart LR A[最早的同步执行] --> B[yield: 可以暂停] B --> C[send: 可以通信] C --> D[yield from: 可以嵌套协作] D --> E[asyncio: 统一调度] E --> F[async/await: 语法封装]

举个栗子

"假设你需要写一个爬虫,先请求 A 接口获取 token,再用 token 请求 B 接口获取数据,最后用数据请求 C 接口。如果用同步方式,每个请求都要等 3 秒,总共 9 秒。但 9 秒里 CPU 大部分时间在等待网络 IO。如果能在这段时间切去做别的事呢?这就引出了协程要解决的问题。"

Python 的协程,差不多就是在这种现实压力里一步一步长出来的。

一、最早的时候,yield只是一个用来暂停的。

很多人第一次见到 yield,都会觉得这东西很奇怪。函数写着写着,居然还能停下来,下次继续执行。它不像普通函数,也不像循环,更不像我们熟悉的"执行完就结束"。

但如果把它放到现实里看,其实很容易理解。

你去食堂排队打饭,轮到你了,先把餐盘放上去,然后厨师开始做。做饭这段时间里,你人站在窗口前发呆,完全没必要。聪明一点的方式是什么?是拿个号,先让后面的人继续,等轮到你的菜好了,再回来取。

yield 干的事,很像这个"拿号"的动作。它让出执行权,把当前状态保存下来,等以后再回来继续。

最早的 Python 协程,其实就借了这个思路。函数执行到一半时,不是彻底结束,而是把当前现场保留下来:局部变量还在,执行位置也还在,下一次再从中断点接着往下走。

flowchart LR A[函数开始执行] --> B[运行到 yield] B --> C[保存当前状态] C --> D[把控制权交出去] D --> E[外部代码继续执行] E --> F[再次唤醒协程] F --> G[从上次中断位置继续]

比如下面这种写法:

python 复制代码
def worker():
    print("开始")
    yield
    print("继续")

第一次看时会觉得怪,但本质上,它不是暂停,而是把控制权交给你接下来代码。

这个阶段的协程,还很原始。它能停,但怎么停、什么时候停、停了以后谁来叫它继续,都不是 Python 帮你管的。也就是说,yield 像是给了你一个暂停权限,但剩下要做的还得自己想办法。

所以那个时代的协程,更准确地说,其实更像"可暂停的生成器"。它很有价值,但还不够完整。

二、回来以后怎么接着跑?

有了 yield 之后,问题并没有结束,反而开始变得更实际了。

因为一旦你真的开始用它做任务切换,就会发现一个很关键的问题:协程不是只会停一下,它还得能接收外部信息。换句话说,它不只是单向输出,还得能和外面"对话"。

最典型的就是 send()

yield 负责把值抛出去,send() 负责把值送回来。这个组合一出现,整个东西的气质就变了。它不再只是"我停一下",而是"我停一下,顺便把东西传给你,你再告诉我下一步怎么做"。

这就很像实际工作里的协作。

比如你在做一个数据处理流程,前面一段负责拉取数据,后面一段负责清洗和分类。中间的状态不可能一直写死在代码里,很多时候需要边执行边传递信息。send() 让这种互动成为可能,协程开始具备一点"通信能力"了。

也正因为这样,早期玩协程的人,经常会写出一些看起来很不像普通 Python 的代码。它们有点像状态机,有点像事件驱动,又有点像手工调度器。能跑,但不优雅;能用,但心里没底。

这时候你会明显感觉到一个事实:yield 解决的是"暂停执行",但没有解决"复杂协程之间如何组合"的问题。

一个任务停下来了,另一个任务怎么接?多层协程嵌套了,谁转发值?谁负责把异常抛回去?谁负责处理返回值?这些细节如果都让开发者手写,代码会很快变成一团线。

就好像让前端控制内存一样

三、yield from 出来以后,协程终于开始像个体系了

如果说 yieldsend() 让 Python 协程有了雏形,那么 yield from 的出现,就像是把这个雏形真正串起来了。

它解决的问题非常现实:协程嵌套以后,别再每层都手工转发了。

在没有 yield from 的时候,如果一个协程要把控制权交给另一个子协程,写法会非常啰嗦。你得手动遍历、手动传值、手动捕获异常、手动把返回值拿回来。写到最后,你会发现自己像是在写协程的"中转站",而不是业务逻辑。

yield from 的意义,就是把这个中转过程交给语言层处理。

你可以把它理解成一种"代理"。上层协程不再亲自做所有转接工作,而是把一部分执行权直接委托出去。这样一来,代码就不那么碎了,协程之间的衔接也自然了很多。

从工程角度看,这一步非常关键。因为它让协程不再只是一个"能暂停的函数",而开始具备"层级组合能力"。

现实中的业务流程本来就是分层的。一个接口请求里可能套着认证、缓存、重试、日志、解析、数据库写入;一个爬虫里也可能套着抓取、解码、清洗、存储。没有 yield from 之前,这些层次关系会在代码里显得很乱;有了它以后,层级结构终于能被比较干净地表达出来。

所以你会发现,协程发展到这里,已经不只是语法技巧了,它开始像一种编程模型。它背后真正想解决的,是"如何在单线程里管理大量等待中的任务",而不是单纯为了写法炫酷。

四、asyncio 出现后,Python 才真正有了所谓的"调度中心"

前面那些东西都很重要,但它们还有一个共同点:都偏底层,偏手工,偏"你得自己安排"。

真正把协程带进日常开发的,是 asyncio

它最大的变化不是"又多了一套 API",而是终于有了一个统一的事件循环。也就是说,任务不再各自为战,而是交给一个调度中心统一管理。

这很像火车站或者机场的塔台。

以前每个任务都像一辆车,自己决定什么时候出发、什么时候停、什么时候让路。很自由,但也很乱。asyncio 的事件循环出现后,就像是有了总调度员:谁先执行,谁去等待,谁在 I/O 完成后被唤醒,全部由它来安排。

在这里,协程真正开始和 I/O 结合得非常紧密。

因为协程最适合的场景,本来就是"等待很多、计算很少"的任务。比如网络请求、文件读写、数据库连接、消息推送、长连接服务。这些任务的大部分时间都不是在做真正的计算,而是在等外部资源响应。线程在这里并不总是最划算的方案,因为线程切换有成本,栈空间也有开销,数量一多管理就麻烦。

协程的优势就在这里体现出来了:它不是抢着跑,而是在不阻塞的前提下,把等待时间尽可能利用起来。

你可以把它理解成一种更精细的时间管理方式。CPU 真正忙的时候,让它干计算;I/O 等待的时候,让别的任务先上。这样一来,程序的资源利用率就会高很多。

不过 asyncio 也带来了一个现实问题:语法开始变得稍微"别扭"了。

如果继续沿用生成器那套写法,代码很容易变成老派风格,读起来不够直接。于是,Python 很自然地又往前走了一步。

"到了这里,协程才真正从手工切换,进入了统一调度的阶段。"

flowchart TD A[创建事件循环] --> B[注册多个协程任务] B --> C[任务运行] C --> D{遇到 I/O 等待?} D -- 是 --> E[挂起当前任务] E --> F[事件循环切换到其他任务] F --> C D -- 否 --> G[继续执行任务] G --> H[任务完成]

什么?看不懂流程图?

什么?看不懂长篇大论?

栗子如下:

python 复制代码
import asyncio
import time

async def fetch_data(url):
    """模拟网络请求:等 2 秒才返回数据"""
    print(f"开始请求 {url}")
    await asyncio.sleep(2)  # 模拟 I/O 等待,不阻塞其他任务
    print(f"请求完成 {url}")
    return f"{url} 的数据"

async def main():
    # 同时发起三个请求,而不是一个一个等
    start = time.time()
    
    results = await asyncio.gather(
        fetch_data("api/user"),
        fetch_data("api/order"),
        fetch_data("api/product")
    )
    
    print(f"全部完成,耗时 {time.time() - start:.2f} 秒")
    print(results)

# 事件循环启动
asyncio.run(main())

代码输出结果:

bash 复制代码
开始请求 api/user
开始请求 api/order
开始请求 api/product
(等待约 2 秒...)
请求完成 api/user
请求完成 api/order
请求完成 api/product
全部完成,耗时 2.01 秒
(results 内容略)

上面的代码展示了调度中心的工作方式:

  • asyncio.sleep(2) 模拟 I/O 等待,此时协程主动"让出"CPU
  • 事件循环(调度中心)趁机去执行另外两个 fetch_data
  • 三个请求几乎是同时发起的,总耗时只有 2 秒,而不是 6 秒
  • 如果用传统同步写法,三个请求串行执行,需要 6 秒

这就是"调度中心"的价值:不让 CPU 干等着,哪个任务有空隙就插哪个上去

与之前代码的对比

python 复制代码
# 没有 asyncio 的同步写法(慢)
def sync_fetch(url):
    print(f"开始请求 {url}")
    time.sleep(2)  # 注意:这里是真的阻塞整个线程
    print(f"请求完成 {url}")
    return f"{url} 的数据"

# 三个请求串行,总耗时 6 秒

五、从 yield 到 async/await,本质上是 Python 一直在解决同一个问题

如果把这段历史连起来看,会发现它其实不是几次互不相关的升级,而是同一个问题被不断拆解、不断重构、不断简化的过程。

最早,Python 只是想让函数能停一下,所以有了 yield

后来发现停了以后还得能传值,所以有了 send()

再后来发现协程之间总要嵌套,总要层层转发,所以有了 yield from

等到异步 I/O 的需求真正爆发,语言层面又需要一个统一调度机制,于是 asyncio 出现了。

最后,为了让这一切更像正常代码,更容易读、更容易维护,async/await 把前面所有机制包装了起来。

这条路线其实很像软件工程里常见的演进逻辑:先解决有没有,再解决好不好用,最后解决别人看不看得懂。

也正因为如此,协程这件事从来都不只是一个"性能优化技巧"。它背后体现的是一种很现实的编程哲学:当系统里有大量时间浪费在等待上时,真正需要优化的不是"更快地算",而是"更聪明地等"。

这句话听起来简单,但实际上很多高并发系统、爬虫系统、网络服务系统,拼到最后拼的就是这个。

六、六六六六六六六六总结

写到这里,再回头看 Python 协程的发展,我突然觉得它不像技术升级,更像人长大的过程。

小时候总觉得,事情得一件一件做。写作业的时候不能听歌,吃饭的时候不能看电视,做一件事就只能盯着一件事。后来慢慢长大才发现,真正累人的从来不是事情太多,而是等待太多。

等公交,等消息,等回复,等机会,等一个不知道什么时候才来的结果。

协程干的事情其实很简单:既然有些等待无法避免,那就别站在原地傻等。

网络请求没回来,那就先去做别的;任务阻塞了,那就先让别人往前走一步。它没有消灭等待,只是重新安排了等待。

仔细想想,人也是一样。

很多事情努力了也不会立刻有结果。有些答案今天得不到,有些目标这一年也到不了。成长好像不是一直往前冲,而是学会在停下来的时候,也别浪费时间。

yieldasync/await,表面看是在优化程序执行效率,实际上更像是在教程序一件事:

世界上最聪明的办法,可能不是拼命往前跑,而是知道什么时候暂停,什么时候等待,什么时候继续出发。

技术发展到最后,优化的不只是代码,有时候也会顺手解释一点生活。

相关推荐
浩风祭月6 小时前
把慢查询日志扔给 AI,从分析到修复只用了半小时:一份完整的实操手册
后端·ai编程
jay神6 小时前
基于YOLOv8的交通标志识别Web系统
前端·人工智能·深度学习·yolo·机器学习·毕业设计
CAD老兵6 小时前
一张 HTML 走天下:CAD-Viewer 首创的「离线 CAD 看图」
前端·javascript·github
程序员榴莲6 小时前
Python 中的 @property:像访问属性一样调用方法
开发语言·前端·python
yingyima6 小时前
Linux定时任务:crontab vs systemd timer,到底谁更适合你的业务?
前端
有味道的男人7 小时前
1688 跨境 API:多语言、跨境代采、独立站商品同步方案
java·服务器·前端
索西引擎7 小时前
【实战】Changesets:Monorepo 版本管理与变更日志的实践
前端
大熊程序猿7 小时前
ASP.NET Core 认证授权:JWT与OAuth2实战
后端·asp.net
Highcharts.js7 小时前
AI向量知识谱系图表创建示例代码|Highcharts网络图表(networkgraph)搭建案例
开发语言·前端·javascript·网络·信息可视化·编辑器·highcharts