如何在循环中正确运行asyncio/aiohttp异步请求?
Hey there! 你已经用协程把请求速度提了10倍,这已经很棒了,但锁占比过高确实是影响进一步优化的关键问题。咱们一步步来拆解你的疑问,先验证实现正确性,再解决锁的问题:
协程并发请求:正确性验证与锁优化指南
一、先确认你的协程实现是否真的“正确异步”
你提到通过遍历列表加队列实现并发,这里有几个常见的坑,你可以对照自查:
- 有没有混用同步HTTP库? 如果你的协程里还是调用
requests.get()(纯同步阻塞),那所谓的“并发”其实是伪异步——速度快10倍可能只是队列批量调度减少了一些串行等待,但本质还是在逐个等请求完成,而且队列的同步锁会让你花大量时间在锁状态。正确姿势是用aiohttp或者httpx.AsyncClient这类异步HTTP客户端。 - 队列用对了吗? 如果用的是
queue.Queue(线程安全但对协程不友好),而不是asyncio.Queue,那每次put()/get()都会触发线程锁竞争,这大概率是你90%时间卡在锁的元凶!asyncio.Queue是专为协程设计的,用协程级别的等待替代了线程锁,完全没有锁竞争开销。 - 有没有控制并发数? 10+并发如果没做限流(比如用
asyncio.Semaphore),瞬间发起大量请求可能触发目标服务器的限流策略,导致后续请求被挂起,看起来像是锁等待。
二、锁占比过高的核心原因分析
你说“并发请求完成后仍有90%的时间处于锁状态”,结合你的流程(启动→并发→锁定5秒→完成),大概率是结果收集或队列管理环节的同步阻塞:
- 同步队列的锁竞争:
queue.Queue的操作都是线程阻塞的,在协程环境里会把整个事件循环卡住,导致大量时间浪费在抢锁上; - 结果收集用了同步容器:比如用普通
list加threading.Lock来存结果,每次写入都要抢锁,自然占比极高。
三、优化方案(附实战代码)
1. 替换为异步友好的组件,抛弃手动队列
asyncio.gather()已经帮你做好了任务调度和结果收集,完全不需要手动维护队列——手动队列反而容易引入锁问题。示例代码如下:
import asyncio import aiohttp async def fetch_single_url(session, url, semaphore): # 用信号量控制并发数,避免请求过载 async with semaphore: async with session.get(url) as resp: # 根据你的需求替换成json()/content()等 return await resp.text() async def main(url_list, max_concurrent=10): # 初始化信号量控制并发 semaphore = asyncio.Semaphore(max_concurrent) # 复用ClientSession,减少连接建立开销 async with aiohttp.ClientSession() as session: # 批量创建协程任务,用gather统一调度 tasks = [fetch_single_url(session, url, semaphore) for url in url_list] results = await asyncio.gather(*tasks) return results if __name__ == "__main__": test_urls = ["https://example.com"] * 50 results = asyncio.run(main(test_urls)) print(f"完成{len(results)}个请求")
2. 避免同步锁的结果收集
上面的代码用asyncio.gather()直接获取结果列表,完全不需要加锁——它是协程级别的同步机制,没有线程锁的开销,效率比手动维护加锁容器高太多。
3. 额外优化点:复用连接池
aiohttp.ClientSession会自动维护连接池,避免每个请求都重新建立TCP连接,这能进一步提升请求速度。
四、如何验证你的实现是否正确?
你可以通过这几个小方法确认:
- 打印每个请求的开始/结束时间,看是否有时间重叠(有重叠才是真并发);
- 开启
asyncio.debug()模式运行,检查是否有未被正确挂起的同步阻塞操作; - 对比总耗时:如果是真异步,总耗时应该接近单个请求的耗时(而不是请求数×单个耗时)。
内容的提问来源于stack exchange,提问作者Defenestrator6
相关产品推荐
相关产品推荐

