首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python的异步编程把我搞崩溃了,原来await加不加完全是两种人生

Python的异步编程把我搞崩溃了,原来await加不加完全是两种人生

原创
作者头像
风一样的男子
发布于 2026-09-14 14:12:26
发布于 2026-09-14 14:12:26
1790
举报
文章被收录于专栏:编程教程编程教程

讲个真事儿。

去年我写了一个异步爬虫,要抓取几千个网页。代码大概长这样:

代码语言:javascript
复制
import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = [...]  # 几千个网址
    async with aiohttp.ClientSession() as session:
        for url in urls:
            html = fetch(session, url)   # 注意这里没加 await
            print(len(html))

跑起来之后,程序瞬间打印了几千行 len(html),每行都是同一个数字。我一开始还以为网速飞快,结果仔细一看,所有输出都是 0 或者一个奇怪的小数字。更诡异的是,服务器端根本没有收到任何请求。

我盯着代码看了十分钟,才发现问题:fetch(session, url) 是一个异步函数,调用它只会返回一个协程对象,不会真正执行。我把它赋值给 html,然后 len(html) 自然就报错了——但为什么没报错?因为协程对象居然有 __len__ 吗?不,其实 len() 会直接抛 TypeError。但我当时用的是 print(len(html)),而 html 是一个协程对象,len() 确实会报错。可为什么我没看到报错?因为我用了 try/except 包住了整个循环,把所有异常都吞掉了。

把 except 去掉之后,满屏的 TypeError: object of type 'coroutine' has no len()。那一刻我才真正理解:在 async 函数里调用另一个 async 函数,不加 await,就等于买了一张电影票但没去电影院——票是有了,电影没放。

async/await 到底在干什么

要理解 await 加与不加的区别,得先明白 Python 异步编程的基本模型。

当你用 async def 定义一个函数时,它就不再是一个普通函数了,而是一个协程函数。调用它不会执行函数体,而是返回一个协程对象。这个协程对象像一个“任务说明书”,里面写着“如果你让我跑,我会做这些事情”。

代码语言:javascript
复制
async def say_hello():
    print("Hello")

# 调用协程函数,返回协程对象,不会打印任何东西
coro = say_hello()
print(coro)  # <coroutine object say_hello at 0x...>

要让协程真正执行,必须把它交给事件循环。最直接的方式就是用 await:

代码语言:javascript
复制
async def main():
    await say_hello()  # 现在才会打印 Hello

asyncio.run(main())

await 的作用可以概括为两点:

  1. 执行:告诉事件循环“把这个协程跑起来”。
  2. 等待:挂起当前协程,直到被等待的协程完成,并获取它的返回值。

所以,await 既是“启动开关”,也是“等待信号”。不加 await,协程对象就只是一个对象,永远不会被执行。

不加 await 的几种典型翻车现场

翻车一:以为调用了,其实没执行

代码语言:javascript
复制
async def save_data(data):
    await db.insert(data)
    print("保存成功")

async def main():
    save_data({"name": "张三"})   # 忘记 await
    print("主函数结束")

asyncio.run(main())

输出:

代码语言:javascript
复制
主函数结束

save_data 根本没跑,数据库里什么也没写。更坑的是,Python 不会报错,只会在程序退出时给一个警告:RuntimeWarning: coroutine 'save_data' was never awaited。如果你没开警告,或者警告被日志淹没了,这个 bug 可能几个月都发现不了。

翻车二:把协程对象当结果用

代码语言:javascript
复制
async def get_user(user_id):
    return await db.fetch_one(user_id)

async def main():
    user = get_user(1)   # 没加 await
    print(user.name)     # AttributeError: 'coroutine' object has no attribute 'name'

你拿到的是一个协程对象,不是用户数据。对它做任何属性访问、索引、运算,都会报错。而且错误信息往往让人摸不着头脑。

翻车三:在列表推导里忘记 await

代码语言:javascript
复制
async def main():
    results = [fetch(url) for url in urls]  # 全是协程对象
    for r in results:
        print(r)  # 打印一堆 <coroutine object ...>

正确的写法要么用 await asyncio.gather(*[fetch(url) for url in urls]),要么在循环里逐个 await。

翻车四:在非 async 函数里调用 async 函数

代码语言:javascript
复制
def sync_func():
    result = async_func()  # 这里不能加 await,因为 sync_func 不是 async
    return result

await 只能出现在 async def 函数内部。在普通函数里,你只能得到协程对象,要么用 asyncio.run() 跑,要么用 asyncio.create_task() 调度,要么用 loop.run_until_complete()。

await 的本质:让出控制权

理解了“不加 await 不执行”之后,还要理解“加了 await 之后发生了什么”。

当你在一个协程里 await 另一个协程时,当前协程会暂停,把控制权交还给事件循环。事件循环可以去执行其他任务。等到被等待的协程完成后,事件循环再把控制权还回来,当前协程从暂停处继续执行。

这就是异步并发的核心:一个线程可以同时处理多个任务,靠的就是在等待 I/O 时主动让出控制权。

代码语言:javascript
复制
async def task_a():
    print("A 开始")
    await asyncio.sleep(1)
    print("A 结束")

async def task_b():
    print("B 开始")
    await asyncio.sleep(1)
    print("B 结束")

async def main():
    await asyncio.gather(task_a(), task_b())

asyncio.run(main())

输出:

代码语言:javascript
复制
A 开始
B 开始
(等待 1 秒)
A 结束
B 结束

两个任务并发执行,总共只花了 1 秒。如果不用 await,或者用同步方式写,就需要 2 秒。

关键点:await 让协程在等待时“让路”,这样事件循环才能去跑别的协程。不加 await,协程根本不会启动,也就没有“让路”这回事。

await 和 asyncio.gather 的区别

很多人分不清 await 和 asyncio.gather。

  • await coro:等待一个协程完成,并拿到它的返回值。
  • await asyncio.gather(coro1, coro2, ...):并发运行多个协程,等待它们全部完成,返回结果列表。

gather 里面传入的是协程对象,不是 await 后的结果。因为 gather 自己会去调度这些协程。

代码语言:javascript
复制
# 正确:并发执行
results = await asyncio.gather(fetch(url1), fetch(url2))

# 错误:这样会串行执行,而且 fetch 返回的是协程对象,不是结果
results = [await fetch(url1), await fetch(url2)]

第二种写法虽然也能跑,但它是串行的:先等 fetch(url1) 完成,再等 fetch(url2)。失去了并发的意义。

什么时候可以不加 await

有一种情况,你故意不加 await,那就是创建任务。

asyncio.create_task(coro) 会把协程包装成一个 Task,立即调度到事件循环中执行,但不会等待它完成。你可以继续做别的事情,稍后再通过 await task 来获取结果。

代码语言:javascript
复制
async def main():
    task1 = asyncio.create_task(fetch(url1))
    task2 = asyncio.create_task(fetch(url2))
    # 这里两个请求已经在后台跑了
    result1 = await task1
    result2 = await task2

这相当于“先点火,后等待”。如果你直接 await fetch(url1),那就变成“点一个火,等它烧完,再点下一个”。

一个完整的对比例子

假设你要从三个 URL 抓取数据,然后合并结果。

错误写法(忘记 await):

代码语言:javascript
复制
async def main():
    results = []
    for url in urls:
        data = fetch(url)      # 没加 await
        results.append(data)   # 追加的是协程对象
    print(results)             # 一堆 <coroutine object>

串行写法(加了 await,但没并发):

代码语言:javascript
复制
async def main():
    results = []
    for url in urls:
        data = await fetch(url)   # 一次等一个
        results.append(data)

总共耗时 = 三个请求时间之和。

并发写法(正确使用 await 和 gather):

代码语言:javascript
复制
async def main():
    tasks = [fetch(url) for url in urls]
    results = await asyncio.gather(*tasks)

总共耗时 = 最慢的那个请求的时间。

为什么 Python 要这样设计

你可能会问:为什么不像 JavaScript 那样,调用 async 函数就自动开始执行?为什么非要加 await?

因为 Python 的协程是显式的。调用一个 async 函数只是创建了一个协程对象,并没有把它注册到事件循环里。只有当你 await 它,或者用 create_task、gather 等工具显式调度它时,它才会运行。

这种设计的好处是控制力更强。你可以自由决定什么时候启动任务、什么时候等待、要不要并发。坏处就是容易忘记加 await,导致“代码没跑”这种低级错误。

如何避免踩坑

几个实用的建议:

  1. 开启警告:Python 默认会为未 await 的协程发出 RuntimeWarning。确保你的日志级别能看到这些警告。
  2. 使用 linter:像 pylint、flake8-async、ruff 都能检测出未 await 的协程调用。
  3. 类型注解:给协程函数标注返回类型,比如 async def fetch() -> str:,这样类型检查工具能帮你发现错误。
  4. 统一风格:在代码审查时,特别留意 async def 的调用处。看到 async 函数被调用,先问一句:“await 了吗?”
  5. 用 asyncio.run() 作为入口:确保最外层用 asyncio.run(main()),这样事件循环会正常运行,未 await 的协程也会在退出时给出警告。

总结

await 加与不加,区别就是“执行”和“不执行”。

  • 不加 await:调用 async 函数只会返回一个协程对象,函数体不会执行。你拿到的是一个“待办事项”,而不是结果。
  • 加 await:协程被调度执行,当前协程暂停等待,直到它完成并返回结果。

理解这一点,就理解了 Python 异步编程的第一道门槛。记住一句话:async 函数不是用来直接调用的,而是用来 await 的。 下次写异步代码时,看到 async def,先检查调用处有没有 await——如果没有,要么补上,要么用 create_task 显式调度。

我那个爬虫后来改成了 asyncio.gather 并发抓取,几千个网页几分钟就跑完了。而那个忘记加 await 的 bug,也成了我之后每次写异步代码时都会下意识检查的第一件事。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • async/await 到底在干什么
  • 不加 await 的几种典型翻车现场
  • await 的本质:让出控制权
  • await 和 asyncio.gather 的区别
  • 什么时候可以不加 await
  • 一个完整的对比例子
  • 为什么 Python 要这样设计
  • 如何避免踩坑
  • 总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档