async/await中修改全局变量会出错是因为协程共享同一全局变量且无自动锁机制,导致读-改-写竞态;必须用全局复用的asyncio.lock保护临界区,或改用queue、封装类等更健壮方案。

为什么 async/await 里改全局变量会出错
因为 async 函数在事件循环中被调度执行,多个协程可能交替运行在同一个线程上——它们共享同一份全局变量,但没有自动加锁。一旦两个协程同时读-改-写(比如 counter += 1),结果就不可预测,哪怕用了 await 也不行。
典型现象:并发跑 100 个 async_inc(),最后 global_counter 却不是 100;日志打印顺序混乱;某些修改直接丢失。
- 这不是 GIL 能拦住的——GIL 只保 Python 字节码原子性,而
a += b至少拆成读、算、写三步 -
asyncio.Lock是最轻量且正确的解法,别试图用threading.Lock(它会阻塞事件循环) - 如果只是读操作,且变量不会被其他协程修改,那可以不加锁;但只要涉及“读取后决定怎么写”,就必须锁
用 asyncio.Lock 安全修改全局计数器
必须显式创建并复用同一个 asyncio.Lock 实例,不能每次进函数都新建一个。
import asyncio <p>global_counter = 0 counter_lock = asyncio.Lock() # ← 全局单例,不是每次 new!</p><p>async def async_inc(): global global_counter async with counter_lock: # ← 必须用 async with,不是 with current = global_counter await asyncio.sleep(0.001) # 模拟异步耗时操作 global_counter = current + 1 </p>
-
async with lock:是唯一安全的进入方式;await lock.acquire()+lock.release()容易漏释放,不推荐 - 锁粒度要细:只包裹真正需要互斥的代码段,比如上面的读-改-写,而不是把
await asyncio.sleep()也包进去(除非业务逻辑要求) - 锁对象不能是局部变量,否则每个协程拿到的是不同锁,完全无效
替代方案:用 asyncio.Queue 或状态封装代替全局变量
全局变量本身是设计坏味道,尤其在异步上下文中。更健壮的做法是把状态收束到可控对象里。
例如用 asyncio.Queue 做任务缓冲:
queue = asyncio.Queue() <p>async def producer(): for i in range(10): await queue.put(i)</p><p>async def consumer(): while True: item = await queue.get()</p><h1>处理 item,无需担心竞态</h1><pre class="brush:php;toolbar:false;"><code> queue.task_done()</code>
-
asyncio.Queue内部已用asyncio.Lock实现线程/协程安全,直接用即可 - 如果状态较复杂,定义一个带
asyncio.Lock的类封装:class Counter: def __init__(self): self._val = 0; self._lock = asyncio.Lock() - 避免跨模块引用全局变量——把状态作为参数传入协程,或通过依赖注入传递,可测性、可维护性都更好
常见误用:用 threading.local() 或普通 dict 混淆作用域
threading.local() 在 async 场景下完全失效,因为多个协程跑在同一线程里,local 对象会被所有协程共享;而用 dict 存协程 ID 当 key,又容易因取消、超时导致 key 泄漏。
- 不要用
asyncio.current_task().get_name()当 key——任务名不唯一,且未设名时返回None - 不要用
id(asyncio.current_task())当 key——任务对象销毁后 ID 可能复用,引发脏数据 - 真要按协程隔离状态,用
contextvars.ContextVar,但它解决的是“协程局部变量”,不是“全局共享状态”的同步问题
真正难的不是加锁,而是判断哪些变量确实需要全局共享——多数时候,你其实并不需要它。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











