asyncio.lock 必须在同一协程中 acquire 和 release,跨协程 release 会静默失败导致死锁;应优先使用 async with lock:,仅保护纯内存操作,避免在临界区内 await i/o;不可重入,禁止嵌套 acquire;释放前需 await 且建议检查 locked()。

asyncio.Lock 必须在同一个协程中 acquire 和 release
跨协程调用 release() 会静默失败,锁永远不释放——这是死锁最隐蔽也最常见的原因。因为 asyncio.Lock 不是全局句柄,它内部记录的是“当前持有锁的 Task”,不是“有没有被锁住”这个状态。
常见错误场景:
- 在
async with lock:块里启动新Task(比如asyncio.create_task(...)),然后试图让那个 Task 调用lock.release() - 把
lock.release()丢进loop.run_in_executor执行(同步函数里调用异步锁的 release) - 用
await lock.acquire()后,在另一个协程里手动调用lock.release()
正确做法:所有 acquire() 和 release() 必须成对出现在同一 async def 函数内;更稳妥的是直接用 async with lock:,它会在退出时自动释放,哪怕中间抛异常也不会漏。
别在 await 语句中间持有 asyncio.Lock
锁只该保护纯内存操作,比如更新 dict、递增计数器、切换状态标志。一旦你在 async with lock: 块里执行 await httpx.AsyncClient().get(...) 或 await aiomysql.Connection.execute(...),就等于把整个临界区拖成了 I/O 等待时间——其他协程全卡在 acquire() 上干等,而你还在等网络或数据库响应。
解决思路:
- 把 I/O 操作移出
async with lock:块,只锁住真正需要互斥的变量读写部分 - 如果业务逻辑确实要求串行化 I/O(比如限流调第三方 API),改用
asyncio.Semaphore(1),语义更清晰,且不会误导人以为“这是个内存锁” - 检查是否真需要锁:很多场景其实用原子操作(如
asyncio.Queue、asyncio.Event)或无锁结构(如单例初始化用if not _initialized: _initialized = True+asyncio.Lock保护赋值)更合适
警惕嵌套 acquire 导致的自死锁
asyncio.Lock 默认不可重入。同一个协程连续两次 await lock.acquire(),第二次就会无限挂起——不报错、不警告,只是卡住。现象通常是:协程进了 async with lock:,又调了另一个也尝试获取同一把锁的异步函数。
排查与应对:
- 加日志:在
acquire()前打印lock.locked()和asyncio.current_task(),确认是不是自己正拿着锁还去抢 - 避免在锁内调用可能再次申请同一锁的函数;若无法避免,考虑拆分锁粒度(比如按资源 ID 创建不同
Lock实例) - 不要手动管理
acquire()/release()配对,尤其不要在finally块里裸调lock.release()——未持有锁时调用会抛RuntimeError: Lock is not acquired,但容易被吞掉
释放前务必检查 lock.locked() 状态
lock.locked() 返回 True 只表示“有某个 Task 持有它”,不保证是当前 Task。所以不能靠它做条件加锁(比如 if not lock.locked(): await lock.acquire() 是错的),但可用于防御性释放。
安全释放模式:
- 优先用
async with lock:,它已内置全部保障 - 若必须手动管理,释放前加判断:
if lock.locked(): await lock.release() - 注意:
lock.release()是协程函数,必须await,不能当普通函数调用
最容易被忽略的一点:asyncio.Lock 的行为和线程锁完全不同,它不提供任何跨协程协作能力,也不支持超时、中断或所有权转移。把它当成“只能在本协程里开、关的门禁卡”,而不是“谁都能按一下的释放按钮”。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











