asyncio.Lock 必须在同一个协程中 acquire 和 release,跨协程释放会静默失败导致死锁;应仅保护内存操作,避免在锁内 await I/O;禁用嵌套 acquire,释放前检查 locked() 状态,并优先使用 async with 确保异常安全。

asyncio.Lock 必须在同一个协程中 acquire 和 release
死锁最常见原因:在协程 A 中 acquire(),却试图在协程 B(甚至同步函数)里调用 release()。Lock 不是线程安全的“全局句柄”,而是协程上下文感知的——它记录的是“哪个 task 持有它”。跨协程释放会静默失败,锁永远不释放。
- 错误写法:
await lock.acquire()后启动新 task 调用lock.release(),或丢给loop.run_in_executor - 正确做法:所有
acquire()和release()必须成对出现在同一async def函数内,且确保执行路径全覆盖 - 更稳妥的方式是用
async with lock:,它自动处理异常时的释放,避免遗漏
别在 await 语句中间持有 asyncio.Lock
一旦 await 某个可挂起对象(比如 await asyncio.sleep(1)、await httpx.AsyncClient().get(...)),当前协程让出控制权,但锁仍被占用——此时其他协程全被堵住,而你还在等 I/O,等于主动制造长时阻塞点。
- 典型陷阱:在
async with lock:块里调用耗时 await,比如数据库查询、HTTP 请求、文件读写 - 解决思路:只把真正需要互斥的“内存操作”包进锁,比如更新共享字典、递增计数器;I/O 操作移出锁作用域
- 如果必须串行化 I/O(如限流访问某 API),考虑用
asyncio.Semaphore(1)替代Lock,语义更清晰,也更容易排查等待链
警惕嵌套 acquire 导致的自死锁
asyncio.Lock 默认不可重入:同一个协程重复 await lock.acquire() 会永远挂起自己——没有递归计数,也不报错,就是卡住。
调用 Cutout.Pro 视觉处理 API 进行背景移除、人像抠图和照片增强,支持文件上传与图片 URL 输入。
- 现象:协程进入
async with lock:后,又调用另一个也尝试获取同一lock的异步函数 - 检查方法:打印
lock.locked()状态,或用asyncio.current_task()对比 owner(需 patch 或调试模式) - 对策:要么改用
asyncio.Lock(locked=False)+ 手动管理(不推荐),要么拆分锁粒度,或用不同锁实例隔离逻辑域
释放前务必确认 lock 处于 acquired 状态
对未持有的锁调用 release() 会抛出 RuntimeError: Lock is not acquired,但这个错误常被吞掉(比如在 finally 里裸调 release() 却没 catch),导致后续逻辑异常难定位。
立即学习“Python免费学习笔记(深入)”;
- 安全写法:总是先查
if lock.locked(): lock.release(),或直接依赖async with的保障 - 注意:
lock.locked()返回True只表示“已被某个 task 持有”,不保证是当前 task —— 所以不能靠它做条件加锁,只能用于防御性释放 - 测试时建议在
async with lock:外故意触发一次release(),验证是否真能捕获并处理异常
最容易被忽略的是:Lock 的生命周期和 task 生命周期强绑定,一旦 task 被 cancel,持有锁的协程可能中断在 acquire() 或 release() 中间,造成锁滞留。这时候得靠 try/except CancelledError + 显式清理,或者统一用 async with 配合 asyncio.shield() 保护关键段。


















