asyncio中+=不是原子操作,因其在字节码层面拆解为读取、计算、写回三步,协程在await点可能让出控制权,导致多协程并发执行时出现竞态;需用asyncio.Lock保护临界区,但锁不能替代逻辑正确性。

asyncio 中的 += 为什么不是原子操作
协程切换不等于线程切换,但一样会打断非原子操作。Python 的 += 在字节码层面拆成三步:读取当前值 → 计算新值 → 写回变量。即使在单线程的事件循环里,await 点(比如 await asyncio.sleep(0))也可能触发协程让出控制权,导致另一个协程读到中间状态。
-
counter += 1实际等价于counter = counter + 1,两次读写分离 - 若两个协程同时执行该语句,且都在读取后、写入前被挂起,最终只加了一次
- 这种问题在高并发计数、缓存更新、状态标记等场景下极易复现,且难以稳定复现
用 asyncio.Lock 保护共享变量是否足够
够,但必须用对。锁只保证临界区串行执行,不解决“读-改-写”本身逻辑缺陷。比如你用锁包住 counter += 1,能避免并发覆盖;但若业务逻辑是“仅当 counter 时才加”,那锁只能保原子性,不能保业务条件的一致性——你需要把整个判断+赋值放进锁内。
- 锁必须包裹所有依赖共享状态的读写操作,不能只锁写不锁读
-
async with lock:是推荐写法,异常时自动释放,避免死锁 - 锁粒度要细:全局锁会严重降低并发度;按资源维度分锁(如按用户ID哈希分桶)更合理
有没有比锁更轻量的替代方案
有,但要看场景。对于纯计数,asyncio.Queue 或专用原子计数器(如 aiostream.stream.count)更安全;对于状态机类更新,推荐用不可变对象 + asyncio.Lock 替换整个引用;而像 Redis 这类外部服务自带原子命令(INCR、HINCRBY),直接用比自己实现锁更可靠。
- 避免在协程里用
threading.Lock—— 它会阻塞事件循环 -
asyncio.Event和asyncio.Condition适合信号通知类同步,不适用于数据修改保护 - 若共享的是 dict/list,别直接修改,改用
asyncio.Lock包裹.update()或.append()调用
return_exceptions=True 会掩盖脏数据问题吗
不会掩盖,但会延迟暴露。这个参数只影响 asyncio.gather() 对异常的传播行为,和数据竞争无关。真正危险的是:你在多个协程里并发修改同一变量,又没加锁,结果某次运行刚好没出错,你以为逻辑正确——直到压测时突然发现订单重复扣款或库存超卖。
立即学习“Python免费学习笔记(深入)”;
- 异常捕获解决的是“失败可见性”,锁解决的是“状态一致性”
- 日志里看到
ValueError: counter went negative,大概率说明你漏了锁,而不是异常没捕获 - 最隐蔽的脏数据发生在“读取旧值→校验通过→写入新值”这个三步链路上,哪怕加了锁,也要确保这三步在同一个锁块里



















