Python asyncio 并发请求里最容易踩的 4 个坑
【摘要】 最近在写一个批量调用接口的小脚本,从 requests 换到 asyncio + aiohttp 之后速度确实上去了,但也连着踩了几个坑,记录一下。在协程里调用了阻塞函数time.sleep()、requests.get()、普通的文件读写都会卡住整个事件循环,其他协程全部停下来。等待用 await asyncio.sleep()HTTP 用 aiohttp 或 httpx 的异步客户端实在...
最近在写一个批量调用接口的小脚本,从 requests 换到 asyncio + aiohttp 之后速度确实上去了,但也连着踩了几个坑,记录一下。
- 在协程里调用了阻塞函数
time.sleep()、requests.get()、普通的文件读写都会卡住整个事件循环,其他协程全部停下来。
- 等待用 await asyncio.sleep()
- HTTP 用 aiohttp 或 httpx 的异步客户端
- 实在绕不开的阻塞调用,用 await asyncio.to_thread(func, *args) 丢到线程里
- 一次性 gather 几千个任务
asyncio.gather(*[fetch(u) for u in urls]) 写起来很爽,但几千个请求同时发出去,要么被对方限流,要么本地连接数打满报错。加一个信号量控制并发:
sem = asyncio.Semaphore(20)
async def fetch(session, url):
async with sem:
async with session.get(url) as resp:
return await resp.text()
- 每个请求都新建 ClientSession
ClientSession 内部有连接池,每次新建等于放弃复用连接,还会出现 Unclosed client session 的警告。整个程序共用一个就够了:
async with aiohttp.ClientSession() as session:
results = await asyncio.gather(*(fetch(session, u) for u in urls))
- 一个任务报错,结果全丢
gather 默认遇到第一个异常就抛出,其他已经成功的结果拿不到。加上 return_exceptions=True,再逐个判断:
results = await asyncio.gather(*tasks, return_exceptions=True)
for url, r in zip(urls, results):
if isinstance(r, Exception):
print(f"{url} 失败: {r}")
Python 3.11 以后也可以用 asyncio.TaskGroup,写法更清晰,但它的策略是一个失败就取消其余任务,适合"要么全成功要么全失败"的场景,按需选择。
小结:异步不是加个 async 就变快,关键是别阻塞事件循环、控制并发、复用连接、处理好异常。希望对刚上手 asyncio 的同学有帮助。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)