asyncio.Lock 必须在同一个协程中 acquire 和 release,跨协程 release 会静默失败导致死锁;正确做法是成对使用或用 async with 自动释放,且仅保护纯内存操作,I/O 应移出临界区。

asyncio.Lock 必须在同一个协程里 acquire 和 release
跨协程调用 lock.release() 会静默失败,锁永远不释放——这是生产环境最隐蔽的死锁源头。因为 asyncio.Lock 内部只记录“当前持有锁的 Task”,不是“是否被锁住”的状态。
常见错误包括:
- 在
async with lock:块里用asyncio.create_task()启动新协程,再让那个协程调lock.release() - 把
lock.release()丢进loop.run_in_executor()执行(同步函数里调异步锁) -
await lock.acquire()后,在另一个协程里手动调lock.release()
正确做法:所有 acquire() 和 release() 必须成对出现在同一 async def 函数内;更稳妥的是直接用 async with lock:,它会在退出时自动释放,哪怕中间抛异常也不会漏。
别在 async with lock: 里 await I/O 操作
asyncio.Lock 只该保护纯内存操作,比如更新 dict、递增计数器、切换状态标志。一旦你在临界区里执行 await httpx.AsyncClient().get() 或 await aiomysql.Connection.execute(),整个临界区就被拖成了 I/O 等待时间——其他协程全卡在 acquire() 上干等。
立即学习“Python免费学习笔记(深入)”;
解决思路:
- 把 I/O 操作移出
async with lock:块,只锁住真正需要互斥的变量读写部分 - 如果业务逻辑确实要求串行化 I/O(比如限流调第三方 API),改用
asyncio.Semaphore(1),语义更清晰,且不会误导人以为“这是个内存锁” - 检查是否真需要锁:很多场景其实用原子操作(如
asyncio.Queue、asyncio.Event)或无锁结构(如单例初始化用if not _initialized: _initialized = True+ 锁保护赋值)更合适
永远给 lock.acquire() 加超时,且区分两种 timeout
asyncio.Lock 默认不提供超时机制。一旦某个协程持锁后崩溃、未释放,或 await 了另一个也依赖该锁的协程,整个链就卡死。
必须强制加超时:
- 永远不要写
await lock.acquire()—— 改为await asyncio.wait_for(lock.acquire(), timeout=5.0) - 锁内逻辑也要套
asyncio.wait_for(..., timeout=...),否则锁占着不放 -
asyncio.TimeoutError是业务超时,可降级或重试;asyncio.CancelledError是运行时中断,通常不该被except捕获,否则会吞掉取消信号,导致死锁
嵌套 acquire 会导致自死锁,且不报错
asyncio.Lock 默认不可重入。同一个协程连续两次 await lock.acquire(),第二次就会无限挂起——不报错、不警告,只是卡住。现象通常是:协程进了 async with lock:,又调了另一个也尝试获取同一把锁的异步函数。
容易被忽略的关键点:
- 检查所有可能间接调用同一把锁的路径,尤其是封装了锁逻辑的工具函数
- 若需重入语义,应自行实现计数器 +
Task.current_task()校验,或换用asyncio.Semaphore(但注意它也不重入) - 调试时可用
lock.locked()在关键位置打日志,确认锁状态是否符合预期



















