Python asyncio 踩了一周坑,我把能犯的错全犯了一遍

上周我接了个私活,需求不复杂:写个爬虫批量抓取几百个页面,然后做数据清洗入库。同步写法跑了一版,慢到我怀疑人生------500 个请求串行跑了将近 8 分钟。果断上 asyncio 改异步。

然后就开始了长达一周的踩坑之旅。

说实话,asyncio 这东西,文档看着挺简单,async defawait 就完了嘛。但真写起来,各种反直觉的行为能让你怀疑自己是不是学了假 Python。这篇文章就是我这一周踩过的真实坑位记录,都是血泪教训,希望能帮到正在入坑的朋友。

先说结论

坑位 症状 原因 解决耗时
协程没被 await 函数调用了但没执行 忘了 await,只拿到协程对象 10 分钟
同步代码阻塞事件循环 整个程序卡住 在 async 函数里调了 time.sleep 2 小时
asyncio.run() 套娃 报错 RuntimeError 在已运行的事件循环里再调 asyncio.run() 半天
gather 异常吞没 部分任务静默失败 默认不会抛出其他任务的异常 1 天
aiohttp session 没关 ResourceWarning 刷屏 没用 async with 管理生命周期 30 分钟
并发太猛被封 IP 429 Too Many Requests 没做并发控制 大半天

坑 1:协程没 await,代码静悄悄地不执行

新手第一个会踩的坑,我也没逃过。

python 复制代码
import asyncio

async def fetch_data():
 print("开始请求...")
 await asyncio.sleep(1)
 print("请求完成")
 return {"data": "hello"}

async def main():
 # ❌ 错误写法:忘了 await
 result = fetch_data()
 print(f"结果: {result}")

asyncio.run(main())

运行结果:

lua 复制代码
结果: <coroutine object fetch_data at 0x...>
RuntimeWarning: coroutine 'fetch_data' was never awaited

fetch_data 里的两个 print 一个都没执行。因为 fetch_data() 只是创建了一个协程对象,并没有真正运行它。

python 复制代码
async def main():
 # ✅ 正确写法
 result = await fetch_data()
 print(f"结果: {result}")

这个坑虽然简单,但在复杂项目里特别隐蔽。比如你在某个回调里调用了协程函数但忘了 await,那段逻辑就直接被跳过了,还不报错(只有个 Warning),debug 的时候你会疯。

坑 2:同步阻塞炸掉整个事件循环

这个坑是真让我排查了两小时。

python 复制代码
import asyncio
import time

async def task_a():
 print(f"[{time.strftime('%H:%M:%S')}] Task A 开始")
 # ❌ 用了同步的 time.sleep
 time.sleep(3)
 print(f"[{time.strftime('%H:%M:%S')}] Task A 完成")

async def task_b():
 print(f"[{time.strftime('%H:%M:%S')}] Task B 开始")
 await asyncio.sleep(1)
 print(f"[{time.strftime('%H:%M:%S')}] Task B 完成")

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

asyncio.run(main())

你猜输出什么?

css 复制代码
[14:00:00] Task A 开始
[14:00:03] Task A 完成 # 注意:B 在 A 完成后才开始!
[14:00:03] Task B 开始
[14:00:04] Task B 完成

time.sleep(3) 直接把事件循环阻塞了,Task B 根本没法并发。asyncio 是单线程的协作式并发,你用同步阻塞调用,就相当于一个人霸占了整条路,别人谁都过不去。

graph TD A[事件循环 - 单线程] --> B{当前协程是否 await?} B -->|是 - 让出控制权| C[切换到其他就绪的协程] B -->|否 - 同步阻塞| D[整个事件循环卡住] D --> E[所有其他协程都在等] C --> F[多个协程交替执行 ✅] E --> G[变成串行执行 ❌]

正确做法:

python 复制代码
async def task_a():
 print(f"[{time.strftime('%H:%M:%S')}] Task A 开始")
 # ✅ 用异步的 sleep
 await asyncio.sleep(3)
 print(f"[{time.strftime('%H:%M:%S')}] Task A 完成")

但现实中不只是 sleep 的问题。你用了 requests 库发 HTTP 请求,用了 open() 读大文件,用了某个不支持异步的数据库驱动------这些全是同步阻塞操作,会把你的事件循环卡得死死的。

如果实在要用同步库,用 run_in_executor 扔到线程池:

python 复制代码
import asyncio
import requests

async def fetch_sync_api(url):
 loop = asyncio.get_event_loop()
 # 把同步的 requests.get 扔到线程池执行
 response = await loop.run_in_executor(None, requests.get, url)
 return response.text

坑 3:asyncio.run() 嵌套调用直接炸

这个坑我是在 Jupyter Notebook 里踩的。

python 复制代码
import asyncio

async def inner():
 return "hello"

async def outer():
 # ❌ 在协程里再调 asyncio.run()
 result = asyncio.run(inner())
 return result

asyncio.run(outer())

直接报:RuntimeError: asyncio.run() cannot be called from a running event loop

原因很简单:asyncio.run() 会创建一个新的事件循环,但你已经在一个事件循环里了。一山不容二虎,一个线程不容两个事件循环。

Jupyter Notebook 里更坑,因为 Notebook 自带一个运行中的事件循环,你在 cell 里直接写 asyncio.run() 就会炸。

解决方案:

python 复制代码
# 在协程内部,直接 await 就行了,不要再 run
async def outer():
 result = await inner() # ✅
 return result

# 在 Jupyter Notebook 里:
# 方案一:直接 await(Jupyter 支持顶层 await)
result = await inner()

# 方案二:用 nest_asyncio(不太优雅但管用)
import nest_asyncio
nest_asyncio.apply()
asyncio.run(outer())

坑 4:asyncio.gather 的异常处理黑洞

这个坑害我丢了一天数据,真的痛。

python 复制代码
import asyncio

async def task_ok():
 await asyncio.sleep(0.5)
 return "成功"

async def task_fail():
 await asyncio.sleep(0.1)
 raise ValueError("出错了!")

async def task_also_ok():
 await asyncio.sleep(0.3)
 return "也成功了"

async def main():
 # ❌ 默认行为:一个任务抛异常,其他任务的结果就拿不到了
 try:
 results = await asyncio.gather(
 task_ok(), task_fail(), task_also_ok()
 )
 except ValueError as e:
 print(f"捕获到异常: {e}")
 # 但是 task_ok 和 task_also_ok 的结果呢?没了。

asyncio.run(main())

gather 在默认行为下,遇到第一个异常就会把它抛出来,其他任务的结果你拿不到(虽然它们其实已经执行了或者还在执行)。

加上 return_exceptions=True

python 复制代码
async def main():
 # ✅ 异常作为返回值,不会中断其他任务
 results = await asyncio.gather(
 task_ok(), task_fail(), task_also_ok(),
 return_exceptions=True
 )
 
 for i, result in enumerate(results):
 if isinstance(result, Exception):
 print(f"任务 {i} 失败: {result}")
 else:
 print(f"任务 {i} 成功: {result}")

asyncio.run(main())

输出:

复制代码
任务 0 成功: 成功
任务 1 失败: 出错了!
任务 2 成功: 也成功了

2026 年了,更推荐用 TaskGroup(Python 3.11+ 引入的):

python 复制代码
async def main():
 try:
 async with asyncio.TaskGroup() as tg:
 t1 = tg.create_task(task_ok())
 t2 = tg.create_task(task_fail())
 t3 = tg.create_task(task_also_ok())
 except* ValueError as eg:
 for exc in eg.exceptions:
 print(f"捕获: {exc}")

TaskGroup 配合 except*(ExceptionGroup 语法)用起来更清晰,异常处理逻辑更可控。

坑 5:并发数不控制,直接被封

最后一个大坑。我一开始写爬虫的时候,500 个请求直接 gather 一把梭:

python 复制代码
# ❌ 500 个请求同时发出去
tasks = [fetch(url) for url in urls] # 500 个
results = await asyncio.gather(*tasks)

结果瞬间收到一堆 429,IP 还被临时封了。

asyncio.Semaphore 控制并发数:

python 复制代码
import asyncio
import aiohttp

async def fetch(session, url, semaphore):
 async with semaphore: # 信号量控制并发
 async with session.get(url) as response:
 return await response.text()

async def main():
 semaphore = asyncio.Semaphore(10) # 最多同时 10 个请求
 
 async with aiohttp.ClientSession() as session:
 tasks = [fetch(session, url, semaphore) for url in urls]
 results = await asyncio.gather(*tasks, return_exceptions=True)
 
 success = sum(1 for r in results if not isinstance(r, Exception))
 failed = sum(1 for r in results if isinstance(r, Exception))
 print(f"成功: {success}, 失败: {failed}")

asyncio.run(main())

顺带一提:aiohttp.ClientSession 一定要用 async with 来管理。手动创建但忘了 close,退出时会收到一堆 ResourceWarning: Unclosed client session,不影响功能,但看着烦。

我总结的 asyncio 心智模型

学了一周,我觉得理解 asyncio 的关键就一句话:它是单线程的协作式并发,所有协程共享一个线程,靠 await 主动让出执行权

对比维度 多线程 threading 异步 asyncio
并发模型 抢占式 协作式
切换时机 OS 随时切换 遇到 await 才切换
线程数 多个 1 个
竞态条件 容易出现 几乎不会
阻塞影响 只阻塞当前线程 阻塞整个事件循环
适用场景 CPU 密集 + IO 高并发 IO
调试难度 高(竞态问题) 中(异步思维)

代码 IO 密集(网络请求、数据库查询、文件读写),asyncio 能给你显著的性能提升。我那个爬虫改完之后,500 个请求从 8 分钟降到了 40 秒,提升了 12 倍。

CPU 密集型的(大量计算、图像处理),asyncio 帮不了你,老老实实用 multiprocessing。

小结

回头看这一周,asyncio 的核心概念并不多,难的是那些反直觉的行为和隐蔽的 bug。几条实操建议:

  1. 先搞清楚事件循环的运行机制,不要急着写代码
  2. 检查所有库是否支持异步,不支持的用 run_in_executor 包一层
  3. gather 一定要加 return_exceptions=True,不然数据丢了你都不知道
  4. 并发数必须控制,Semaphore 是你的好朋友
  5. Python 3.11+ 尽量用 TaskGroup 替代 gather,异常处理更优雅

踩完这些坑之后,asyncio 用起来其实挺顺手的。起码比 threading 那套锁来锁去的操作省心多了------毕竟单线程,不用操心竞态条件。

有问题欢迎评论区交流,asyncio + 数据库连接池的实践后面可能也会写一篇。

相关推荐
飞Link2 小时前
LangGraph 核心架构解析:节点 (Nodes) 与边 (Edges) 的工作机制及实战指南
java·开发语言·python·算法·架构
资深设备全生命周期管理3 小时前
EXE Ver 适用于 未安装Python 以及包的Windows OS
python
Lyyaoo.3 小时前
【Java基础面经】Java 反射机制
java·开发语言·python
广州山泉婚姻3 小时前
VSCode中切换Python虚拟环境失败的原因
python
Ulyanov3 小时前
从零构建现代化Python音频播放器:ttk深度应用与皮肤系统设计
python·架构·音视频·数据可视化
吃一根烤肠3 小时前
NumPy 内置函数与数组运算完全指南
python·numpy
Mr_Xuhhh3 小时前
深入理解Java高级特性:反射、枚举与Lambda表达式实战指南
开发语言·python
派大星~课堂4 小时前
【力扣-94.二叉树的中序遍历】Python笔记
笔记·python·leetcode
SQVIoMPLe4 小时前
python-langchain框架(3-7-提取pdf中的图片 )
python·langchain·pdf