前端 Python 3.12

5. 内置类型与线程安全

无 GIL(自由线程)构建下 list/dict/set/bytearray/memoryview 的并发保证分级;与默认 CPython 的差异及实践表。

本章对应官方 Thread Safety Guarantees(线程安全保证):说明在 关闭 GIL、使用自由线程(free-threaded)构建 的 Python 中,内置类型上哪些操作自带同步、哪些必须靠外部锁。默认带 GIL 的发行版里,多数字节码在解释器层面已隐式串行化,不能把本章当成「有 GIL 就可以随便多线程改同一个 list」的借口——业务上仍应按共享可变状态来设计锁或消息传递。

更一般的写法与已知限制见官方 Python 对自由线程的支持(与 PEP 703PYTHON_GIL 等配套说明)。

1 C-API 文档中的安全级别

级别从弱到强排列(与官方一致):

级别含义
Incompatible(不兼容)即使用外部锁也难以安全并发;通常碰进程级全局状态(信号处理、环境变量等),程序生命周期内宜单线程调用。
Compatible(兼容)多线程可调用,但调用方须持锁包住每次调用;无锁则可能有数据竞争。
Safe on distinct objects无外部锁时,只要各线程操作不同对象(或不共享底层缓冲)即安全;同一对象并发仍须锁。
Safe on shared objects同一对象并发也安全;实现内部用每对象锁或临界区等保护。
Atomic(原子)在其他线程看来瞬时完成,是最强保证。

2 list:要点表

操作要点
lst[i](读一项)原子
item in lstlst.indexlst.count每对象锁;遍历中依赖对各项的原子读,可能看到并发修改下的中间态
lst[i] = x(写一项)多线程调用不破坏 list 结构(官方表述)。
lst1 + lst2n * lstlst.copy()返回新对象;对他线程呈原子观感。
lst.append(x)lst.pop()(末尾)无元素搬移时 原子clear() 原子;排序 sort() 非原子(排序期间他线程可能看到 list 像空的一样,但看不到排序中间排列)。
insertpop(i)(非末尾)、lst *= n原地搬移多块,可能与无锁遍历交错,可见中间态
remove比较可能走 __eq__(任意 Python 代码),存在与其它线程交错的空间。
extend / += iterablelist/tuple/set/frozenset/dict/字典视图(且非子类重写迭代)等,官方对 iterable 一侧有更强保证;一般迭代器仍可能被别线程改。
lst[i:j] = iterable多线程可调;iterablelist(非子类)时另有锁定约定。
读改写、if lst: pop()边迭代边改非原子 / 非线程安全;共享 list 须外部同步或拷贝再迭代。

3 dict:要点表

操作要点
d[key]d.getkey in dlen(d)无锁、原子读类操作。
d[key] =del d[key]poppopitemsetdefault单键写删多线程下不破坏 dict;键比较若走用户 __eq__,可能与并发修改交错(str/int/float 等 C 实现比较期间不释锁,风险低)。
copyd | otherkeys/values/items持每对象锁完成,返回新视图或新 dict。
clear持锁全程,他线程看不到「一半被删」的元素。
update(other)|===other 为标准 dict 迭代时往往两把锁都拿;子类自定义迭代等则不同;与非 dict 可迭代 update只锁目标 dict,对端可被别线程改。
dict.fromkeys参数为 dict/set/frozenset(且非子类)时锁双方;否则只锁新 dict。
读改写、if key in d: delfor ... in d.items()非原子 / 不安全;用 pop(key, None) 等原子替代「先查再删」;迭代用 d.copy().items() 或加锁。

4 set:要点表

操作要点
len(s)无锁、原子
elem in s无锁;可能与持锁写操作交错,见中间态;__eq__ 注意同 dict。
addremovediscardpop持锁单元素变更,不破坏 set;同样有 __eq__ 交错问题。
copyclearcopy 持锁全程原子;clear 持锁全程。
set/frozenset |= &= 等及 isdisjoint/issubset在操作数为 set/frozenset双对象加锁(官方对 update/union/intersection/difference 等有多条细则,见原文)。
check-then-remove、边迭代边改不安全;共享 set 须锁或拷贝迭代。

5 bytearray:要点表

操作要点
len无锁、原子
+、比较走缓冲协议,不持每对象锁,可能看到并发写的中间态
单字节/切片读 ba[i]ba[i:j]多线程安全读。
单字节写、切片赋、append/extend/insert/pop/remove/reverse/clear官方列为多线程下不致损坏 bytearray 的操作(仍注意与无锁 +/比较的交错)。
ba[i:j] = other_bytearray双方加锁。
copyba * n持锁建新对象。
x in ba持锁全程。
find/replace/split/decode一般持每对象锁执行全程。
迭代、if x in ba: remove不安全;可 ba.copy() 再迭代或加锁。

6 memoryview

要点内容
自身元数据自由线程构建下,创建/释放与 shapeformat不变字段的并发读,实现上多用原子操作。
底层缓冲数据属于 exporterbytes 等多线程只读通过多 view 一般安全;bytearray 等可变缓冲,多线程无锁读写同一区域不安全,可能损坏数据。
只读 view不能阻止别线程通过其它引用改 mutable 底层对象。
调整大小在仍有 memoryview 导出时 bytearray.resize 等会 BufferError(与是否多线程无关)。

7 实践建议

场景建议
默认 CPython(有 GIL)仍按「共享可变状态要锁或队列」设计;GIL 不替代模块级、扩展模块或 nogil 未来的语义。
已采用自由线程构建官方 threadsafety 页 为清单,核对用到的 list/dict/set/bytearray/memoryview 操作是否允许无锁共享。
复合操作x = lst[i] + 1lst[i] = lst[i] + 1、先 in 再删等一律视为非原子,需锁或原子 API。
遍历中修改list/dict/set/bytearray 均应用 拷贝;dict 优先 d.copy().items()

资料:线程安全保证

On this page