写Python写久了,你一定遇到过这种情况:程序卡在某处不动了,等网络请求、等文件读写、等数据库返回——CPU闲着,程序却傻等着。
很多人第一反应是上多线程。但Python的多线程有GIL,真正干活儿的只有一个线程,碰上IO密集任务,线程切换的成本还不低。
这时候,异步IO(asyncio)就登场了。
01 先搞清三个概念:并行、并发、异步
这三个词经常被混着用,但实际不是一回事。
并行(Parallelism)——同一时刻做多件事。需要多核CPU,每个核心跑一条线。适合CPU密集任务,比如大规模数学计算、视频渲染。Python里的multiprocessing就是干这个的。
并发(Concurrency)——多个任务"看起来"同时进行。不一定真的在同时跑,而是任务之间切换得够快,给你一种同时进行的错觉。并发比并行范围更广,并行是并发的一种特例。
异步IO(Async IO)——单线程、单进程内实现协作式多任务。每个任务在等待的时候主动"让出"CPU,让其他任务先跑。这是Python asyncio 做的事情。
简单记:并行靠核数,并发靠调度,异步靠"让路"。
02 一个经典比喻:国际象棋大师的异步策略
这个比喻来自Miguel Grinberg的PyCon演讲,是我看过最通俗的解释。
假设国际象棋大师Judit Polgár 要跟24个业余选手同时下棋:
同步版:Judit一次只跟一个人下,下完一盘再下一盘。
每局耗时(55+5)× 30 = 1800秒(30分钟)
24局 = 24 × 30 = 12小时
异步版:Judit走完一桌就换下一桌,让对手在她离开时思考。
把所有24桌的一步走完:24 × 5 = 120秒(2分钟)
总共 = 120 × 30 = 1小时
注意:Judit始终只有一个人,一次只走一步棋。但因为她在对手思考时"切换"到了下一桌,总时间从12小时砍到了1小时。
这就是异步IO的核心思想:不等。别干等别人干完,先去干别的活。
03 异步IO vs 多线程 vs 多进程
很多新手会纠结一个问题:有了多线程,还要异步干嘛?
其实它们解决的场景不同:
|
多进程 |
多线程 |
异步IO |
| 核心机制 |
多个进程 |
多线程轮流执行 |
单线程协作 |
| GIL影响 |
无 |
有 |
无(单线程) |
| 适合场景 |
CPU密集 |
IO密集(少量任务) |
IO密集(大量任务) |
| 任务数上限 |
CPU核心数 |
百级 |
万级 |
| 编写难度 |
中等 |
高(竞态条件) |
中等 |
| 内存开销 |
大 |
中 |
小 |
当你的IO任务特别多——比如爬10000个网页——多线程很可能撑不住,而asyncio完全扛得住。
原因很简单:线程是操作系统资源,10000个线程光创建切换就能把机器拖死。而asyncio的协程本质是用户态调度,10000个协程在内存里就是个列表。
04 Python的并发模型演进
Python标准库在支持并发上走过了一条很长的路:
threading —— 多线程,但要面对GIL和竞态条件multiprocessing —— 多进程,绕过GIL,但进程间通信复杂concurrent.futures —— 封装了线程池/进程池
从Python 3.5开始,async和await成为语言的关键字,asyncio也成了标准库的一员。到了Python 3.11+,异常组(ExceptionGroup)等新特性进一步提升了异步编程的体验。
说实话,异步IO并不简单——回调、协程、事件循环、future、协议、传输……光术语就能劝退一堆人。但好在这些年文档和生态已经成熟了很多,不再是"劝退"级别的难度了。
05 什么时候用异步IO?
记住一句话:IO密集 + 大量任务 = asyncio的好时机。
具体场景:
不适合的场景:
另外注意:不是所有IO库都支持异步。比如标准文件操作就是阻塞的——要在异步代码里读写文件,得用aiofiles这种专门的异步库。数据库也一样,需要支持async/await语法的驱动。
小结
这一篇我们理清了异步IO是什么、跟其他并发模型有什么区别、什么时候该用它。
说实话,异步IO没那么玄乎。它的核心就一个字——等。不是傻等,而是趁等的功夫去干别的。跟多线程比起来,它更轻量、更可控、更适合大量IO任务的场景。
下一篇,我们直接上手写代码——协程、async/await、事件循环,一步到位。
关注「Bug与灵光」,追更Python全系列教程。有问题欢迎留言,咱们下一篇见。