dict.setdefault()非原子操作,执行分检查键、插入默认值、返回值三步,多线程下易致重复初始化与竞态丢失;GIL不保障其线程安全,应使用Lock或专用并发结构。

dict.setdefault() 本身不是原子操作
dict.setdefault() 看似简单,但它的执行分三步:检查键是否存在 → 若不存在则插入默认值 → 返回对应值。这三步在 CPython 中**不构成原子操作**,中间可能被其他线程打断。一旦多个线程同时对同一个 key 调用 setdefault(),就可能出现重复计算默认值、覆盖写入,甚至逻辑错误。
典型并发问题:重复初始化与竞态丢失
常见于缓存初始化场景,比如用字典做单例对象池:
cache = {}
def get_worker(name):
return cache.setdefault(name, Worker(name)) # ❌ 并发下可能创建多个 Worker当两个线程同时发现 name 不存在,都会执行 Worker(name) 构造函数,然后各自写入——后写的会覆盖先写的,但构造开销已浪费,还可能引发资源泄漏(如重复建连接)。
- 现象:
Worker.__init__()被调用多次,但cache[name]只保留最后一次结果 - 根本原因:读-判-写(read-check-write)非原子,且默认值表达式(
Worker(name))在锁外求值 - 注意:
dict的底层哈希表扩容也可能在并发写入时触发未定义行为(虽不常崩,但标准不保证安全)
安全替代方案:threading.Lock 或 collections.defaultdict(仅限无副作用默认值)
若默认值构造有副作用(如 IO、实例化、状态变更),必须加锁;若只是常量或无状态工厂,可考虑 defaultdict,但它仍不能解决“首次赋值竞态”——因为 defaultdict 的 __missing__ 也是在查不到时动态调用,同样非原子。
Shopxp购物系统历经多年的考验,并在推出shopxp免费购物系统下载之后,收到用户反馈的各种安全、漏洞、BUG、使用问题进行多次修补,已经从成熟迈向经典,再好的系统也会有问题,在完善的系统也从在安全漏洞,该系统完全开源可编辑,当您下载这套商城系统之后,可以结合自身的技术情况,进行开发完善,当然您如果有更好的建议可从官方网站提交给我们。Shopxp网上购物系统完整可用,无任何收费项目。该系统经过
- ✅ 推荐:用
threading.Lock包裹整个 check-and-set 逻辑 - ✅ 更优:改用
concurrent.futures.ThreadPoolExecutor+functools.lru_cache(需可哈希参数)或weakref.WeakValueDictionary配合显式同步 - ⚠️ 注意:
dict.setdefault(key, lock.acquire() or value or lock.release())这类写法是错的——acquire()返回True/False,且锁没释放
CPython GIL 不能帮你绕过这个问题
GIL 只保证单个字节码指令的原子性,而 setdefault() 对应多条字节码(LOAD_METHOD + CALL_METHOD),GIL 会在调用过程中释放(尤其在默认值含 IO 或 sleep 时)。所以即使纯 Python 场景,也不能依赖 GIL 实现线程安全。
真正需要并发安全字典行为时,别试图给 dict 打补丁——直接换用 threading.local()(线程隔离)、concurrent.futures.as_completed()(任务级协调),或引入 redis/memcached 做外部协调。本地 dict 的并发读写,从来就不是它的设计目标。









