Python 异步编程入门:用 asyncio 把串行 IO 变成并发

举报
早睡早起的Mikey 发表于 2026/11/04 00:30:55 2026/11/04
【摘要】 asyncio 让 Python 用事件循环调度协程,把多次网络等待重叠成一次等待。本文从协程基础讲到 gather 并发、wait_for 超时、在异步代码里调用阻塞库的坑,以及什么时候该上异步。

一、先分清并发与并行

同步代码里,程序按语句顺序往下走,一个 HTTP 请求没返回,下一句就不会开始。这在本地脚本里没问题,但一旦搬到服务端,线程的大部分时间都花在「等网络」上,CPU 只能空转。

异步的目标不是让单次计算更快,而是把等待时间重叠起来:在 A 接口还在等响应的时候,先去发 B 接口的请求。这两类工作(等待 IO / 计算)被拆开之后,同样的一秒钟里就能同时推进多条链路。

二、协程:async def 与 await

Python 3.5 之后,语言层面原生支持协程。关键只有两个关键字:async def 声明一个协程函数,await 表示「我在这里挂起,把 CPU 让给别人,等结果回来再继续」。

import asyncio

async def fetch(name):
    await asyncio.sleep(1)   # 模拟一次网络等待
    return f'{name} done'

async def main():
    print(await fetch('a'))

asyncio.run(main())

asyncio.run() 会创建事件循环、把这个协程丢进去、并在退出前把循环关掉。理解「事件循环」很重要:它是这个模型里唯一真正在跑的东西,协程只是被它调度的一段段代码。

三、并发执行:asyncio.gather

如果只是把几个协程写成 await fetch('a') 一路串起来,实际效果和同步代码没区别——还是等完 a 再等 b。真正的并发要靠 gather 或 create_task 把它们同时排进队列。

import asyncio, time

async def fetch(name, delay):
    await asyncio.sleep(delay)
    return name

async def main():
    start = time.time()
    # 串行:总耗时约 3 秒
    for n, d in [('a', 1), ('b', 1), ('c', 1)]:
        await fetch(n, d)

    # 并发:总耗时约 1 秒
    result = await asyncio.gather(
        fetch('a', 1), fetch('b', 1), fetch('c', 1)
    )
    print(result, time.time() - start)

asyncio.run(main())

gather 接受一组协程(或 Task),等它们全部完成后按顺序返回一个结果列表;只要有一个抛异常,默认行为是立刻把异常抛出来。想让单个失败不影响其它任务,可以传 return_exceptions=True,此时异常会作为结果元素返回,而不是中断整体。

四、超时控制:别让一次慢请求拖垮整个请求

真实环境里对端可能不响应。裸 await 会永远等下去,生产代码里几乎总要配一个上限。

async def main():
    try:
        data = await asyncio.wait_for(fetch('slow', 10), timeout=2)
    except asyncio.TimeoutError:
        data = None
    print(data)

wait_for 超时时会取消内层任务并抛出 TimeoutError。如果内层任务不支持取消,调用方可能会等到任务真正结束才拿到控制权——这是异步编程里最常见的一类「幽灵阻塞」。

五、在异步代码里调用阻塞库

现成的第三方库大多还是同步的(比如老的 HTTP 客户端、数据库驱动)。在事件循环里直接调用它们,会把整个循环卡住。Python 3.9 起提供了 asyncio.to_thread,把阻塞调用丢到线程池里执行:

import asyncio

def blocking_read(path):
    with open(path) as f:
        return f.read()

async def main():
    content = await asyncio.to_thread(blocking_read, 'data.txt')
    print(len(content))

同理,CPU 密集型任务(如压缩、图像处理)也不该在事件循环里跑,用 run_in_executor 交给进程池往往比线程池更合适。

六、几个容易踩的坑

  • 忘了 await:直接调用协程函数只创建了一个协程对象,不会执行。Python 会给出 coroutine was never awaited 警告。批量场景请用 gather 或 create_task。
  • 在协程里跑阻塞代码:一次 requests.get() 就足以让同一事件循环里的其它请求全部排队。要么换异步库,要么用 to_thread 包一层。
  • Task 引用被回收:create_task 返回的 Task 若没有变量持有,事件循环只持有弱引用,可能在执行前就被 GC 掉,结果就是「任务凭空消失」。用 set 兜一层是常见写法。
  • 取消不是强制中断:被取消的协程如果屏蔽了 CancelledError,或者卡在不可中断的同步调用里,取消就会失效。清理逻辑放在 finally 里,并且不要吞掉 CancelledError。

七、小结

异步编程的收益来自「把等待重叠起来」,代价是心智负担上升:多了一层调度、取消语义、还有同步库混用时的坑。判断要不要上异步,可以问三个问题:

  1. 是不是有大量独立的 IO 等待?
  2. 是不是需要同时维持很多并发连接?
  3. 团队能不能接受更高的调试成本?

如果只是偶尔调一两个外部接口,同步代码加线程池往往更简单,也更容易维护;如果是上百个并发连接、或者要长时间维持长连接,异步模型的收益才真正体现出来。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。