跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

在多线程中不加锁使用 defaultdict 是否安全

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
到不同对象上,只有一个留在字典里 安全替代方案:按场景选最小开销方式 不需要全局锁,但必须保证“查 + 设”原子性: Python 3.14.3 微软官方的 Python 扩展,是 VS Code 安装量最高的扩展(209M+)。集成 IntelliSense(通过 Pylance)、调试(通过 Python Debugger)、代码检查、格式化、重构和单元测试等功能。支持 Jupyter Notebook、虚拟环境管理和多 Python 版本切换。 下载 用
dd.setdefault(k, factory())
替代直接访问
dd[k]
—— 它在 C 层做了原子插入,但注意:工厂函数仍会在每次调用时执行,只是返回值可能被丢弃;所以工厂函数必须无副作用 对计数类场景,优先用
threading.local()
配合局部
defaultdict
,最后再合并;避免竞争 真要共享状态且高频更新,用
threading.Lock
包裹整个读-改-写过程;粒度可细化到每个键(用
collections.defaultdict(threading.Lock)
管理键级锁),但要注意死锁和内存增长 Python 3.9+ 可考虑
weakref.WeakKeyDictionary
配合锁,减少长生命周期锁对象残留 验证是否出问题不能只靠测试 竞态条件往往在高并发、低延迟或特定调度下才暴露。仅跑几次单元测试几乎肯定通过,但生产环境可能几小时才出现一次数据错乱。真正可靠的判断依据是:代码逻辑中是否存在「非原子的多步字典操作」,而不是有没有复现过错误。 最容易被忽略的一点:很多人以为“我只读不写就安全”,但
defaultdict
的读操作(
__getitem__
)一旦命中缺失键,就会写——它本质是读写混合操作。

相关文章