5. 内置类型与线程安全
无 GIL(自由线程)构建下 list/dict/set/bytearray/memoryview 的并发保证分级;与默认 CPython 的差异及实践表。
本章对应官方 Thread Safety Guarantees(线程安全保证):说明在 关闭 GIL、使用自由线程(free-threaded)构建 的 Python 中,内置类型上哪些操作自带同步、哪些必须靠外部锁。默认带 GIL 的发行版里,多数字节码在解释器层面已隐式串行化,不能把本章当成「有 GIL 就可以随便多线程改同一个 list」的借口——业务上仍应按共享可变状态来设计锁或消息传递。
更一般的写法与已知限制见官方 Python 对自由线程的支持(与 PEP 703、PYTHON_GIL 等配套说明)。
级别从弱到强排列(与官方一致):
| 级别 | 含义 |
|---|
| Incompatible(不兼容) | 即使用外部锁也难以安全并发;通常碰进程级全局状态(信号处理、环境变量等),程序生命周期内宜单线程调用。 |
| Compatible(兼容) | 多线程可调用,但调用方须持锁包住每次调用;无锁则可能有数据竞争。 |
| Safe on distinct objects | 无外部锁时,只要各线程操作不同对象(或不共享底层缓冲)即安全;同一对象并发仍须锁。 |
| Safe on shared objects | 对同一对象并发也安全;实现内部用每对象锁或临界区等保护。 |
| Atomic(原子) | 在其他线程看来瞬时完成,是最强保证。 |
| 操作 | 要点 |
|---|
lst[i](读一项) | 原子。 |
item in lst、lst.index、lst.count | 无每对象锁;遍历中依赖对各项的原子读,可能看到并发修改下的中间态。 |
lst[i] = x(写一项) | 多线程调用不破坏 list 结构(官方表述)。 |
lst1 + lst2、n * lst、lst.copy() | 返回新对象;对他线程呈原子观感。 |
lst.append(x)、lst.pop()(末尾) | 无元素搬移时 原子;clear() 原子;排序 sort() 非原子(排序期间他线程可能看到 list 像空的一样,但看不到排序中间排列)。 |
insert、pop(i)(非末尾)、lst *= n | 原地搬移多块,可能与无锁遍历交错,可见中间态。 |
remove | 比较可能走 __eq__(任意 Python 代码),存在与其它线程交错的空间。 |
extend / += iterable | 对 list/tuple/set/frozenset/dict/字典视图(且非子类重写迭代)等,官方对 iterable 一侧有更强保证;一般迭代器仍可能被别线程改。 |
lst[i:j] = iterable | 多线程可调;iterable 为 list(非子类)时另有锁定约定。 |
读改写、if lst: pop()、边迭代边改 | 非原子 / 非线程安全;共享 list 须外部同步或拷贝再迭代。 |
| 操作 | 要点 |
|---|
d[key]、d.get、key in d、len(d) | 无锁、原子读类操作。 |
d[key] =、del d[key]、pop、popitem、setdefault | 单键写删多线程下不破坏 dict;键比较若走用户 __eq__,可能与并发修改交错(str/int/float 等 C 实现比较期间不释锁,风险低)。 |
copy、d | other、keys/values/items | 持每对象锁完成,返回新视图或新 dict。 |
clear | 持锁全程,他线程看不到「一半被删」的元素。 |
update(other)、|=、== | other 为标准 dict 迭代时往往两把锁都拿;子类自定义迭代等则不同;与非 dict 可迭代 update 时只锁目标 dict,对端可被别线程改。 |
dict.fromkeys | 参数为 dict/set/frozenset(且非子类)时锁双方;否则只锁新 dict。 |
读改写、if key in d: del、for ... in d.items() | 非原子 / 不安全;用 pop(key, None) 等原子替代「先查再删」;迭代用 d.copy().items() 或加锁。 |
| 操作 | 要点 |
|---|
len(s) | 无锁、原子。 |
elem in s | 无锁;可能与持锁写操作交错,见中间态;__eq__ 注意同 dict。 |
add、remove、discard、pop | 持锁单元素变更,不破坏 set;同样有 __eq__ 交错问题。 |
copy、clear | copy 持锁全程原子;clear 持锁全程。 |
与 set/frozenset 的 |= &= 等及 isdisjoint/issubset 等 | 在操作数为 set/frozenset 时双对象加锁(官方对 update/union/intersection/difference 等有多条细则,见原文)。 |
check-then-remove、边迭代边改 | 不安全;共享 set 须锁或拷贝迭代。 |
| 操作 | 要点 |
|---|
len | 无锁、原子。 |
+、比较 | 走缓冲协议,不持每对象锁,可能看到并发写的中间态。 |
单字节/切片读 ba[i]、ba[i:j] | 多线程安全读。 |
单字节写、切片赋、append/extend/insert/pop/remove/reverse/clear 等 | 官方列为多线程下不致损坏 bytearray 的操作(仍注意与无锁 +/比较的交错)。 |
ba[i:j] = other_bytearray | 双方加锁。 |
copy、ba * n | 持锁建新对象。 |
x in ba | 持锁全程。 |
find/replace/split/decode 等 | 一般持每对象锁执行全程。 |
迭代、if x in ba: remove | 不安全;可 ba.copy() 再迭代或加锁。 |
| 要点 | 内容 |
|---|
| 自身元数据 | 自由线程构建下,创建/释放与 shape、format 等不变字段的并发读,实现上多用原子操作。 |
| 底层缓冲 | 数据属于 exporter;bytes 等多线程只读通过多 view 一般安全;bytearray 等可变缓冲,多线程无锁读写同一区域不安全,可能损坏数据。 |
| 只读 view | 不能阻止别线程通过其它引用改 mutable 底层对象。 |
| 调整大小 | 在仍有 memoryview 导出时 bytearray.resize 等会 BufferError(与是否多线程无关)。 |
| 场景 | 建议 |
|---|
| 默认 CPython(有 GIL) | 仍按「共享可变状态要锁或队列」设计;GIL 不替代模块级、扩展模块或 nogil 未来的语义。 |
| 已采用自由线程构建 | 以 官方 threadsafety 页 为清单,核对用到的 list/dict/set/bytearray/memoryview 操作是否允许无锁共享。 |
| 复合操作 | x = lst[i] + 1、lst[i] = lst[i] + 1、先 in 再删等一律视为非原子,需锁或原子 API。 |
| 遍历中修改 | 对 list/dict/set/bytearray 均应用 拷贝 或 锁;dict 优先 d.copy().items()。 |
资料:线程安全保证