asyncio.semaphore.acquire() 卡住主因是任务取消后未释放许可,因取消不触发自动回滚;必须用 async with 确保 release 执行,裸调 acquire/release 易致死锁。

asyncio.Semaphore.acquire() 为什么会在 await 后卡住不返回
当 acquire() 调用后一直挂起,不是因为信号量没释放,而是调用者在未拿到锁时就被取消(比如任务被 asyncio.cancel()),而该取消未触发对应 release() —— 这会导致后续所有 acquire() 等待一个永远不会归还的许可。根本原因:异步信号量本身不自动回滚,acquire() 是协程,取消它不会反向调用 release()。
常见错误场景:
- 在
try/except CancelledError中只做清理但忘了release() - 用
async with semaphore:但中间抛出未被捕获的异常,导致__aexit__无法执行 - 多个
acquire()嵌套但只配对一次release()
必须用 async with 而不是裸调 acquire/release
async with semaphore: 是唯一能保证 release() 执行的惯用写法,它的 __aexit__ 在任何退出路径(正常、异常、取消)下都会被调用。裸调 acquire() + release() 几乎必然出问题,尤其在有超时或取消逻辑时。
正确写法示例:
async def worker(sem: asyncio.Semaphore):
async with sem: # ✅ 自动 acquire + 保证 release
await do_something()
错误写法示例:
async def worker_bad(sem: asyncio.Semaphore):
await sem.acquire() # ❌ 可能 acquire 成功但后续崩溃/取消,release 永远不执行
try:
await do_something()
finally:
sem.release() # ⚠️ 如果 acquire 抛出 CancelledError,这里根本进不来
需要手动 acquire 时,必须用 try/except CancelledError 包裹整个 acquire 块
某些场景确实不能用 async with(比如要控制超时、重试或分阶段获取),此时必须把 await semaphore.acquire() 和后续逻辑包在一个 try/except 中,并显式处理 asyncio.CancelledError。
关键点:
-
CancelledError可能在await acquire()期间抛出,也可能在await acquire()返回后、业务逻辑执行中抛出 - 只有在
acquire()成功返回后才应调用release();如果acquire()本身被取消,不能调用release() - 推荐使用
asyncio.current_task().cancelled()辅助判断当前是否已被取消
简明安全模板:
got = False
try:
await semaphore.acquire()
got = True
await do_work()
finally:
if got:
semaphore.release()
semaphore._value 不是调试依据,别靠它判断死锁
semaphore._value 是内部属性,可能因等待队列、取消状态等出现负值(如 -1 表示有一个协程在等待但尚未分配许可),不能用来判断“还有几个许可可用”或“是否死锁”。死锁真正线索在任务状态:asyncio.all_tasks() 中长期处于 pending 且 stack 显示卡在 await semaphore.acquire() 的任务。
排查建议:
- 用
asyncio.wait(tasks, timeout=1)快速识别挂起任务 - 打印任务
task.get_coro().__name__和task._coro.cr_frame.f_lineno定位卡点 - 避免在
acquire()前加日志——它可能永远不执行;改用asyncio.create_task(..., name="worker-1")配合task.get_name()追踪
真正的死锁往往不在信号量本身,而在业务逻辑里循环依赖了多个 async with 或跨协程误用了同一个 semaphore 实例。共享信号量前先确认作用域和生命周期是否匹配。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











