defaultdict 不是线程安全的,因其底层 dict 的读写及默认工厂调用均非原子操作,多线程下易导致重复创建对象、副作用重复执行或数据丢失;安全做法需保证“查+设”原子性,如用 setdefault 或加锁。
defaultdict 本身不是线程安全的
是
的子类,底层共享 Python 字典的实现。CPython 中普通
的读写操作(包括
、
、
)都不是原子操作;当触发默认工厂函数(如
或
)并插入新键时,实际包含“检查键是否存在 → 调用工厂 → 插入
键值对
”多个步骤,中间可能被其他线程打断。
常见错误现象:
- 多个线程同时访问一个不存在的键,导致工厂函数被调用多次,产生多个独立对象(比如多个空
),但只有其中一个被最终写入字典;
- 更隐蔽的是,如果工厂函数有副作用(如发请求、改全局状态),会被意外重复执行。
不加锁时哪些操作看似安全实则危险
即使只做
或
,也不安全——因为
这一步可能触发默认工厂,而
或
是分开执行的:
触发
得到
,但还没来得及赋值回字典,另一线程也进来,又得到一个
两个线程各自执行
,都试图写回
,结果丢失一次计数
若用
,更糟:两次
可能分别创建两个不同
对象,
到不同对象上,只有一个留在字典里
安全替代方案:按场景选最小开销方式
不需要全局锁,但必须保证“查 + 设”原子性:
Python 3.14.3
微软官方的 Python 扩展,是 VS Code 安装量最高的扩展(209M+)。集成 IntelliSense(通过 Pylance)、调试(通过 Python Debugger)、代码检查、格式化、重构和单元测试等功能。支持 Jupyter Notebook、虚拟环境管理和多 Python 版本切换。
下载
用
替代直接访问
—— 它在 C 层做了原子插入,但注意:工厂函数仍会在每次调用时执行,只是返回值可能被丢弃;所以工厂函数必须无副作用
对计数类场景,优先用
配合局部
,最后再合并;避免竞争
真要共享状态且高频更新,用
包裹整个读-改-写过程;粒度可细化到每个键(用
管理键级锁),但要注意死锁和内存增长
Python 3.9+ 可考虑
配合锁,减少长生命周期锁对象残留
验证是否出问题不能只靠测试
竞态条件往往在高并发、低延迟或特定调度下才暴露。仅跑几次单元测试几乎肯定通过,但生产环境可能几小时才出现一次数据错乱。真正可靠的判断依据是:代码逻辑中是否存在「非原子的多步字典操作」,而不是有没有复现过错误。
最容易被忽略的一点:很多人以为“我只读不写就安全”,但
的读操作(
)一旦命中缺失键,就会写——它本质是读写混合操作。
defaultdictdictdict__getitem____setitem__setdefaultlistintlistdd[k] += 1dd[k].append(x)dd[k]+=.append()dd[k]int()000 + 11dd[k].append(x)dd[k]listappenddd.setdefault(k, factory())dd[k]threading.local()defaultdictthreading.Lockcollections.defaultdict(threading.Lock)weakref.WeakKeyDictionarydefaultdict__getitem__