defaultdict 不是线程安全的,因其底层 dict 的读写及默认工厂调用均非原子操作,多线程下易导致重复创建对象、副作用重复执行或数据丢失;安全做法需保证“查+设”原子性,如用 setdefault 或加锁。

defaultdict 本身不是线程安全的
defaultdict 是 dict 的子类,底层共享 Python 字典的实现。CPython 中普通 dict 的读写操作(包括 __getitem__、__setitem__、setdefault)都不是原子操作;当触发默认工厂函数(如 list 或 int)并插入新键时,实际包含“检查键是否存在 → 调用工厂 → 插入键值对”多个步骤,中间可能被其他线程打断。
常见错误现象:
- 多个线程同时访问一个不存在的键,导致工厂函数被调用多次,产生多个独立对象(比如多个空 list),但只有其中一个被最终写入字典;
- 更隐蔽的是,如果工厂函数有副作用(如发请求、改全局状态),会被意外重复执行。
不加锁时哪些操作看似安全实则危险
即使只做 dd[k] += 1 或 dd[k].append(x),也不安全——因为 dd[k] 这一步可能触发默认工厂,而 += 或 .append() 是分开执行的:
-
dd[k]触发int()得到0,但还没来得及赋值回字典,另一线程也进来,又得到一个0 - 两个线程各自执行
0 + 1,都试图写回1,结果丢失一次计数 - 若用
dd[k].append(x),更糟:两次dd[k]可能分别创建两个不同list对象,append到不同对象上,只有一个留在字典里
安全替代方案:按场景选最小开销方式
不需要全局锁,但必须保证“查 + 设”原子性:
BJXShop网上购物系统是一个高效、稳定、安全的电子商店销售平台,经过近三年市场的考验,在中国网购系统中属领先水平;完善的订单管理、销售统计系统;网站模版可DIY、亦可导入导出;会员、商品种类和价格均实现无限等级;管理员权限可细分;整合了多种在线支付接口;强有力搜索引擎支持... 程序更新:此版本是伴江行官方商业版程序,已经终止销售,现于免费给大家使用。比其以前的免费版功能增加了:1,整合了论坛
- 用
dd.setdefault(k, factory())替代直接访问dd[k]—— 它在 C 层做了原子插入,但注意:工厂函数仍会在每次调用时执行,只是返回值可能被丢弃;所以工厂函数必须无副作用 - 对计数类场景,优先用
threading.local()配合局部defaultdict,最后再合并;避免竞争 - 真要共享状态且高频更新,用
threading.Lock包裹整个读-改-写过程;粒度可细化到每个键(用collections.defaultdict(threading.Lock)管理键级锁),但要注意死锁和内存增长 - Python 3.9+ 可考虑
weakref.WeakKeyDictionary配合锁,减少长生命周期锁对象残留
验证是否出问题不能只靠测试
竞态条件往往在高并发、低延迟或特定调度下才暴露。仅跑几次单元测试几乎肯定通过,但生产环境可能几小时才出现一次数据错乱。真正可靠的判断依据是:代码逻辑中是否存在「非原子的多步字典操作」,而不是有没有复现过错误。
最容易被忽略的一点:很多人以为“我只读不写就安全”,但 defaultdict 的读操作(__getitem__)一旦命中缺失键,就会写——它本质是读写混合操作。









