直接在多个asyncio协程中读写同一list/set会导致竞态条件:丢失插入、重复添加、KeyError、迭代时抛RuntimeError;因GIL不保证复合操作原子性,且协程可在任意字节码处挂起。

asyncio协程间直接用普通list/set会出什么问题
直接在多个 asyncio 协程里读写同一个 list 或 set,不会报错,但结果不可预测——因为 Python 的 GIL 不保证复合操作(如 my_set.add(x); len(my_set))的原子性,更不阻止协程在任意字节码处被挂起。常见现象包括:丢失插入、重复添加、KeyError 即使刚检查过存在、迭代时抛 RuntimeError: Set changed size during iteration。
应该用 asyncio.Queue 还是 asyncio.Lock + 普通集合
选哪个取决于访问模式:
- 如果主要是「生产-消费」流(如任务分发、日志收集),用
asyncio.Queue最自然,它内置线程/协程安全,支持背压和 awaitable 接口 - 如果需要频繁随机查询、成员判断、遍历或原地修改(如维护活跃连接 ID 集合),必须用
asyncio.Lock保护普通set或dict;Queue不支持in检查或len() -
asyncio.Queue是 FIFO,无法替代集合的去重和 O(1) 查找语义
用 asyncio.Lock 保护 set 的正确写法
关键不是「加锁」本身,而是锁的粒度和生命周期要覆盖所有共享访问点,包括读、写、遍历:
import asyncio <p>active_ids = set() lock = asyncio.Lock()</p><p>async def add_id(uid): async with lock: active_ids.add(uid)</p><p>async def remove_id(uid): async with lock: active_ids.discard(uid) # 用 discard 而非 remove,避免 KeyError</p><p>async def has_id(uid): async with lock: return uid in active_ids</p><p>async def get_all_ids(): async with lock: return list(active_ids) # 复制后返回,避免外部修改 </p>
注意:async with lock: 必须包裹每一次对 active_ids 的访问,哪怕只是 len(active_ids) 或 for x in active_ids —— 否则仍可能在迭代中途被其他协程修改集合。
为什么不能用 threading.Lock 或 queue.Queue
threading.Lock 在 asyncio 环境中会阻塞整个事件循环,因为它是同步阻塞原语;queue.Queue 同理,其 get()/put() 是同步方法,调用时会卡住当前协程,破坏异步并发性。错误示例:q = queue.Queue(); await q.get() 会报 TypeError: object Queue can't be used in 'await' expression。唯一可混用的同步原语是 asyncio.to_thread() 包裹,但为集合操作引入线程池得不偿失,纯属绕路。
真正容易被忽略的是:锁保护的是「逻辑一致性」,不是「性能」。即使只读操作(比如检查是否存在),只要和其他写操作共享同一数据结构,就必须加锁——asyncio 中没有「只读无需锁」这回事。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











