python 3.9 并发编程有什么改进

接入与开发

给 Python 开发者讲清 3.9 在并发与异步编程上的实际改进,并给出可落地的排查与调优顺序,不写死数字。

你在一台机器上同时跑几个爬虫或者调几十路大模型接口,工程里写着 asyncio.to_thread 或者 asyncio.run,结果换到 Python 3.9 就报 ImportError 或者事件循环行为对不上。这篇把 3.9 到底改了什么、哪些写法在 3.9 能跑、哪些得自己兜底讲清楚,并给一套可以直接照做的排查顺序。

你在一台机器上同时跑几个爬虫、或者调几十路大模型接口,工程里写着 asyncio.to_thread 或者 asyncio.run,结果换到 Python 3.9 上就报 ImportError,要么事件循环行为跟你在新版本里习惯的那套对不上。这不是你代码写错了,是入口函数和取消语义在不同小版本之间有差异,照抄新版本写法当然会翻车。

这篇不重复讲 GIL 是什么、协程是什么,只讲 3.9 在并发这一层实际动了哪些地方、哪些写法在它上面能跑、哪些得自己兜底,最后给一套可以直接照着走的排查顺序。涉及具体版本行为和每个补丁的差异,别照抄博客,去站内页面对照术语和当时的技术动向,比如 /ai-dict/category/tech 和 /papers。

3.9 在并发这块到底动了哪些地方

改动主要集中在 asyncio 这一层:任务取消与超时的语义更明确了,取消后清理逻辑更可控,写批量请求时不容易出现那种「任务状态明明结束了、连接却没关」的诡异残留。多进程侧主要是 API 收口和资源回收相关的清理,长期跑的后台任务不容易攒下僵尸进程。这两个方向都不是加新玩具,而是把原来模糊的行为讲清楚。

取消语义
取消不是立刻生效,协程要自己在 CancelledError 里做清理,清理完还要把取消信号抛出去
超时语义
超时是事后统一收口,重点是超时之后连接、句柄、子进程有没有真的被关掉
进程回收
多进程侧偏向 API 收口,长时间跑要关注子进程有没有被正确回收

asyncio 的改进:取消、超时与任务管理

超时这套思路,统一走 asyncio.wait_for 或者 async with 一个超时上下文来收口,写法不用纠结哪个更时髦,关键是超时以后要保证连接、文件句柄、子进程都被关掉。并发一高,没关的连接就是泄漏,第二天早上你会看到程序还活着但一个请求都发不出去。判断标准很简单:超时触发之后,对象数是不是落回去了。

  • 任务取消后,协程要在 CancelledError 里做清理;如果取消路径上又 await 了新东西,必须重新抛出取消信号,不能吞掉
  • 用 gather 并发调接口时,建议带上 return_exceptions=True 加自己汇总失败列表,别让一个异常把整批任务都标记成失败
  • 超时不要只写在最外层,里面每一层 await 都要能被打断,否则外层超时了内层还在挂
  • 长连接、连接池、子进程这三类资源,超时后要逐一确认有没有释放
  1. 先确认取消有没有传播在协程里 catch CancelledError 打一行日志,再故意取消一个任务,看日志有没有打出来;打不出来说明取消被吞了
  2. 再确认超时有没有关资源把超时设成明显会触发的值,跑一轮后看连接数、句柄数、子进程数有没有回落,没回落就是泄漏
  3. 最后才怀疑下游被打挂前两步都干净,再降并发数复跑,看错误率是否下降,这一步才轮到怀疑对方接口扛不住

常见并发写法在 3.9 上的对照与替代

你想干的事3.9 上的稳妥写法为什么
把同步函数丢到线程里跑自己包一层线程池,或者用 loop.run_in_executorto_thread / run 这类便捷入口在不同小版本上支持情况不一样,老环境容易 ImportError
限制对外并发数在协程层用 Semaphore比事后靠重试压回来便宜得多,也不会把下游打挂
跑 CPU 密集任务交给 ProcessPoolExecutor塞进事件循环只会把整个 loop 卡住
批量跑大量任务按份数分批,批与批之间留间隔一次性打满短时间好看,长期跑更容易崩

这张表里的判断不是绝对的。如果你的服务本身并发就不高、下游又是自家内网系统,分批和信号量的收益有限,硬加反而增加复杂度。什么时候不适用:单机、低并发、下游无配额限制的场景,直接并发跑完收工就行。具体分几批、间隔留多少,按你的接口额度定,判断口径见 /guides/batch-concurrency-throttle-guide。

升级或新写代码时的落地清单

  1. 写最小复现脚本一个协程、一个超时、一个取消,确认你当前解释器上的实际行为,再去动业务代码,别上来就改大文件
  2. 把参数做成配置项并发数、超时、重试次数不要散在代码里,出问题时要能不改代码就调,改完重启就能验证
  3. 日志留关键字段至少留下任务 id、开始与结束时间、结束原因是取消还是超时,否则事后根本分不清是慢还是卡死
  4. 先量基线再调并发到 /tools/speed-test 量一遍到目标接口的延迟基线,并发调优才有参照物;真实数字别抄别人,按你在线测的为准

这份清单的适用范围是「你准备升级解释器」或者「你要新写一段并发代码」。如果你只是在老项目里补一个小函数、并发量也很低,前两步就够,日志和基线可以省。但一旦这段代码要长期跑在后台,四步都别跳,省下的时间远比不上半夜被叫起来查泄漏。

下一步先把最小复现脚本跑通,再去站内工具页量一遍接口基线,然后按清单逐项对照你的代码。想系统看并发与限流的口径和术语,从导航页进去按分类找。

常见问题

3.9 的并发改动能让我现有的多线程代码变快吗?

基本不会。3.9 的改进主要在 asyncio 与多进程的资源管理上,纯计算型多线程仍然受 GIL 限制,该拆进程还得拆进程。速度提升来自把阻塞调用改成异步、或者把并发数控在合理范围,而不是换个版本号。

我在 3.9 上写 asyncio.to_thread 直接报错,是版本问题吗?

大概率是。这个入口在 3.9 的小版本支持情况不一致,稳妥做法是自己封一层 loop.run_in_executor 或者维护一个线程池,不依赖这个糖。先跑最小复现脚本确认,再决定改代码还是升解释器。

并发一拉高就报超时或限流,该怎么调?

先把并发降到一半看是否恢复,再按接口额度设分档并发与退避重试;同时确认超时之后连接真的被关掉了。限流和并发不是一回事,判断口径可以对照 /guides/concurrency-vs-rate-limit-guide,触发后的恢复步骤见 /guides/api-rate-limit-recovery。

本文依据各平台公开文档与官方公告整理,只做中文归纳;具体做法、参数与限制以官方文档为准。