asyncio.semaphore 是 asyncio 提供的异步信号量,本质是带计数的协程安全锁,初始化时指定最大并发数(如 semaphore(5)),每次 acquire() 阻塞协程直至计数大于0并减1,release() 则加1;它不阻塞线程,只挂起协程,天然适配 async/await,专用于限制同时进入临界区的协程数量,而非控制执行频率或时间间隔。

asyncio.Semaphore 是什么,为什么它适合限流
asyncio.Semaphore 是 asyncio 提供的异步信号量,本质是一个带计数的锁:初始化时指定最大并发数(比如 semaphore = asyncio.Semaphore(5)),每次 acquire() 会阻塞直到计数 > 0,然后减 1;release() 则加 1。它不阻塞线程,只挂起协程,天然适配 async/await 流程。
关键点在于:它只限制「同时进入临界区的协程数量」,不控制执行频率或时间间隔——也就是说,它解决的是“别一下子涌进来太多”,而不是“每秒最多跑几个”。如果你要的是 QPS 限流(如每秒最多 10 次请求),asyncio.Semaphore 单独用是不够的。
常见错误现象:
• 用 asyncio.Semaphore(1) 想实现“串行执行”,结果发现耗时长的任务卡住整个队列,后续协程干等;
• 把 semaphore 放在任务外层(比如 async def main() 里 acquire 一次),导致所有任务共用一个入口,完全失去并发性。
怎么正确包裹异步任务做并发控制
核心原则:每个需要限流的独立异步操作(比如一次 HTTP 请求、一次数据库写入)都应在内部 acquire/release —— 而不是在调度层统一锁住。
import asyncio import aiohttp <p>semaphore = asyncio.Semaphore(3) # 最多 3 个并发请求</p><div class="aritcle_card flexRow artxards"> <div class="artcardd flexRow"> <a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill9386" title="python学习"><img src="https://img.php.cn/upload/skill/000/000/081/179257684888086.jpg" alt="python学习" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a> <div class="aritcle_card_info flexColumn"> <a rel="nofollow" href="/xiazai/skill9386" title="python学习" class="overflowclass">python学习</a> <p class="overflowclass">Python 编程学习助手 skill,面向零基础用户。 当用户表达以下意图时触发: - 学习 Python 基础概念(变量、数据类型、循环、函数、类等) - 请求 Python 练习题或测验 - 请求审查或调试 Python 代码 - 请求 Python 项目实战指导(爬虫、数据分析、Web 开发等入门项目) - 泛化表述如"教我 Python""我想学编程""Python 怎么写""帮我学 Python" 此 skill </p> </div> <a rel="nofollow" href="/xiazai/skill9386" title="python学习" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a> </div> </div><p>async def fetch_url(session, url): async with semaphore: # ✅ 正确:每个请求单独占一个 slot async with session.get(url) as resp: return await resp.text()</p><p>async def main(): urls = ["<a href="https://www.php.cn/link/5f69e19efaba426d62faeab93c308f5c">https://www.php.cn/link/5f69e19efaba426d62faeab93c308f5c</a>"] <em> 10 async with aiohttp.ClientSession() as session: tasks = [fetch_url(session, url) for url in urls] await asyncio.gather(</em>tasks)</p>
- 必须用
async with semaphore:,它自动处理acquire()/release(),避免忘记 release 导致死锁 - 不要在
main()或循环外 acquire —— 那样只是把整个批处理变成串行 - 如果任务有不同优先级或分组(如读/写分离),应为每组创建独立的
asyncio.Semaphore实例
和 time.sleep / asyncio.sleep 混用会怎样
asyncio.Semaphore 不提供节奏控制,有人会试图补上 await asyncio.sleep(0.1) 来“降速”,但这只是让每个任务多等 100ms,不改变并发数,反而拉长总耗时,还可能因网络波动导致实际 QPS 更不稳定。
如果你真需要速率限制(比如 10 req/s),得组合使用:
-
asyncio.Semaphore控制并发数(防压垮服务端) - 额外加一个令牌桶或漏桶逻辑(例如用
asyncio.Queue预生成令牌,或记录上一次执行时间 +asyncio.sleep(max(0, 0.1 - elapsed)))
注意:不要在 async with semaphore: 块内 sleep,否则 slot 被占着不动,白白浪费配额。
生产环境容易忽略的细节
• asyncio.Semaphore 实例不能跨 event loop 复用。如果 fork 进程或重用 loop(比如某些测试框架),需确保 semaphore 绑定到当前 loop;
• 它不感知任务失败:即使某个协程抛异常,async with 仍会 release,这点安全;但若手动 acquire 后没 release(比如未用上下文管理器且中间 raise),会导致计数泄漏,最终所有协程永久阻塞;
• 在高并发场景下,大量协程争抢 semaphore 可能带来轻微调度开销,不过通常远小于 I/O 开销,不必过早优化。
真正难的是区分“我到底要限什么”:是防自己打崩下游(用 Semaphore),还是遵守对方 API 配额(得加时间维度),还是降低本地 CPU/内存压力(可能该调小 batch size 或加 backpressure)。混用目标,限流就变成玄学。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










