线程安全性保证¶
本页面记录了针对 Python 自由线程构建版中内置类型的线程安全保证。 在此描述的保证适用于 Python 禁用了 GIL 的情况(即自由线程模式)。 当启用 GIL 时,大多数操作都是隐式地串行的。
有关如何在自由线程版 Python 中编写线程安全代码的通用指导,请参阅 Python 对自由线程的支持。
线程安全性级别¶
C API 文档使用下列级别来描述每个函数的线程安全性保证。 这些级别按从最低到最高的安全性排列。
不兼容¶
即便使用外部同步化也无法确保并发使用安全性的函数或操作。 不兼容的代码通常会以非同步方式访问全局状态并且在整个程序生命周期中只能从一个线程调用。
例如:会修改进程级状态如信号处理器或环境变量的函数,当从任意线程调用时,即便使用了外部锁机制,也可能与运行时或其他库发生冲突。
兼容的¶
只要 调用方提供了适当的外部同步化,例如通过在每次调用期间持有 lock 锁,即可安全地从多个线程调用的函数或操作。 没有这样的同步化,并发调用可能会产生 竞争条件 或 数据竞争。
例如:针对一个内部状态没有锁保护的对象执行读取或写入的函数。 调用方必须确保不会有两个线程同时访问同一个对象。
在单独对象上安全¶
只要每个线程是在 不同 对象上操作,即可安全地从多个线程调用的函数或操作。 两个线程可以同时调用该函数,但它们不能传入同一个对象(或共享底层状态的不同对象)作为参数。
例如:使用非原子化写入来修改一个结构体的字段的函数。 两个线程可以分别在它们各自的结构体实例上安全地调用该函数,但在 相同 实例上并发调用则需要外部同步化。
原子化的¶
对于其他线程来说是 原子化的 函数或操作 —— 即从其他线程的视角看来是瞬间执行的。 这是线程安全性的最强形式。
例如:对于互斥状态 PyMutex_IsLocked() 会执行原子化的读取因而可在任何时刻从任意线程调用。
列表对象的线程安全性¶
lst[i] # list.__getitem__
下列方法会遍历列表并使用 原子化操作 读取每个条目来执行其函数。 这意味着它们可能返回受并发修改影响的结果:
item in lst
lst.index(item)
lst.count(item)
上述操作都会避免获取 每对象锁。 它们不会阻塞并发的修改。 其他持有锁的操作也不会在观察中间状态时阻塞这些操作。
从这里开始的所有其他操作都会使用 per-object lock 来阻塞执行。
从多个线程通过 lst[i] = x 对单个条目的写入调用是安全的并且不会破坏列表。
下列操作会返回新的对象并且对其他线程来说是 原子化的:
lst1 + lst2 # 将两个列表拼接为一个新列表
x * lst # 将列表重复 x 次成为一个新列表
lst.copy() # 返回列表的浅拷贝
以下仅在单个元素上执行操作而无需变换位置的方法都是 原子化的:
lst.append(x) # 添加到列表末尾,无需变换位置
lst.pop() # 从列表末尾弹出元素,无需变换位置
clear() 方法也是 原子化的。其他线程无法观察到被移除的元素。
sort() 方法不是 原子化的。 其他线程无法观察排序期间的中间状态,在排序期间列表将显示为空。
以下操作可以允许 lock-free 操作来观察中间状态因为它们会原地修改多个元素。
lst.insert(idx, item) # 改变元素位置
lst.pop(idx) # idx 不在列表末尾,会改变元素位置
lst *= x # 原地拷贝元素
remove() 方法可允许并发修改因为元素比较可能执行任意 Python 代码 (通过 __eq__()).
从多个线程调用 extend() 是安全的。不过,这项保证依赖于传给它的可迭代对象。如果它是 list, tuple, set, frozenset, dict 或 字典视图对象 (但不能是它们的子类),对于此可迭代对象进行的并发修改来说 extend 操作是安全的。 在其他情况下,则会创建可被另一个线程并发修改的可迭代对象。这种情况同样适合当使用 lst += iterable 将一个列表与另一个可迭代对象进行原地拼接的场合。
类似地,使用 lst[i:j] = iterable 对一个列表切片赋值的操作从多个线程调用也是安全的,但是 iterable 仅在它也是 list (但不是其子类) 的时候才会被锁定。
涉及到多线程访问,以及迭代的操作一定是非原子化的。例如:
# 非原子化:读取 - 修改 - 写入\nlst[i] = lst[i] + 1
# 非原子化:检测 - 等待 - 执行\nif lst:
item = lst.pop()
# 非线程安全:在修改期间迭代
for item in lst:
process(item) # 其他线程可能修改 lst
当在线程间共享 list 实例时可考虑进行外部同步。
字典对象的线程安全性¶
使用 dict 构造器创建字典在传入的参数为 dict 或 tuple 时是原子化的。 如果使用 dict.fromkeys() 方法,字典创建在参数为 dict, tuple, set 或 frozenset 时是原子化的。.
d[key] # dict.__getitem__
d.get(key) # dict.get
key in d # dict.__contains__
len(d) # dict.__len__
从这里开始的所有其他操作都会持有 per-object lock。
写入或移除单个条目的操作从多个线程调用是安全的并且不会破坏字典:
d[key] = value # 写入
del d[key] # 删除
d.pop(key) # 移除并返回
d.popitem() # 移除并返回最后一项
d.setdefault(key, v) # 如果缺失则插入
这些操作可能会使用 __eq__() 对键进行比较,这可以执行任意 Python 代码。 在这种比较期间,字典可能会被其他线程修改。 对于内置类型如 str, int 和 float,它们是用 C 来实现 __eq__() 的,下层的锁在比较期间不会被释放所以这不是问题。
下列操作会返回新的对象并在操作期间持有 per-object lock:
d.copy() # 返回一个字典的浅拷贝
d | other # 将两个字典合并为一个新字典
d.keys() # 返回一个新的 dict_keys 视图对象
d.values() # 返回一个新的 dict_values 视图对象
d.items() # 返回一个新的 dict_items 视图对象
clear() 方法会在其执行期间持有锁。 其他线程无法观察到元素被移除。
下列操作会同时锁定两个字典。 对于 update() 和 |=,这仅在另一个操作数是使用标准字典迭代器(不能是重写了迭代操作的子类)的 dict 时才适用。 对于相等性比较,这适用于 dict 及其子类:
d.update(other_dict) # 当 other_dict 为字典时双方都会被锁定
d |= other_dict # 当 other_dict 为字典时双方都会被锁定
d == other_dict # 对于字典及其子类双方都会被锁定
所有比较操作也都会使用 __eq__() 来比较值,因此对于非内置类型来说锁可能会在比较期间被释放。
当可迭代对象为 dict, set 或 frozenset (不能为其子类) 时 fromkeys() 会同时锁定新字典和可迭代对象:
dict.fromkeys(a_dict) # 双方均锁定
dict.fromkeys(a_set) # 双方均锁定
dict.fromkeys(a_frozenset) # 双方均锁定
当使用非字典可迭代对象进行更新时,只有目标字典会被锁定。 可迭代对象可能会被其他线程并发地修改:
d.update(iterable) # iterable 不是字典:仅 d 被锁定
d |= iterable # iterable 不是字典:仅 d 被锁定
dict.fromkeys(iterable) # iterable 不是字典 dict/set/frozenset:仅结果被锁定
涉及多线程访问,以及迭代的操作肯定是非原子化的:
# 非原子化:读取-修改-写入
d[key] = d[key] + 1
# 非原子化:先检查再执行(TOCTOU)
if key in d:
del d[key]
# 非线程安全:在修改时迭代
for key, value in d.items():
process(key) # another thread may modify d
要避免检查时和使用时脱节(TOCTOU)问题,应使用原子化操作或处理异常:
# 使用带默认值的而不是先检查再删除
d.pop(key, None)
# 或者处理异常
try:
del d[key]
except KeyError:
pass
要安全地迭代可能会被另一个线程修改的字典,可以迭代其拷贝:
# 拷贝以安全地迭代
for key, value in d.copy().items():
process(key)
在线程间共享 dict 实例时可考虑进行外部同步。
集合对象的线程安全性¶
以下读取操作是免锁的。 它不会阻塞并发修改并可以观察持有每对象锁的操作的中间状态:
elem in s # set.__contains__
此操作可能会使用 __eq__() 对元素进行比较,这可以执行任意 Python 代码。 在这种比较期间,集合可能会被其他线程修改。 对于内置类型如 str, int 和 float, __eq__() 不会在比较期间释放下层的锁因而这不会有问题。
从这里开始的所有其他操作都会持有每对象锁。All other operations from here on hold the per-object lock.
添加或移除单个元素的操作从多个线程调用是安全的并且不会破坏集合:
s.add(elem) # 添加元素
s.remove(elem) # 移除元素,如不存在则引发异常
s.discard(elem) # 元素如存在则移除
s.pop() # 移除并返回任意元素
这些操作也会对元素进行比较,因此也适用与上文相同的 __eq__() 考量。
copy() 方法会返回新的对象并在执行期间持有每对象锁因此它始终是原子化的。
clear() 方法会在其执行期间持有锁。 其他线程无法观察到元素被移除。
下列操作仅接受 set 或 frozenset 作为操作数并且总是同时锁定两个对象:
s |= other # other 必须为 set/frozenset
s &= other # other 必须为 set/frozenset
s -= other # other 必须为 set/frozenset
s ^= other # other 必须为 set/frozenset
s & other # other 必须为 set/frozenset
s | other # other 必须为 set/frozenset
s - other # other 必须为 set/frozenset
s ^ other # other 必须为 set/frozenset
set.update(), set.union(), set.intersection() 和 set.difference() 可接受多个可迭代对象作为参数。 它们将迭代所传入的所有可迭代对象并执行以下操作:
set.update()和set.union()仅对以下情况同时锁定两个操作数
set.intersection()和set.difference()总是会尝试锁定所有对象。
set.symmetric_difference() 会尝试同时锁定两个对象。
以上方法的更新形式也存在一些不同之处:
set.difference_update()和set.intersection_update()会尝试一个接一个地锁定所有对象。
下列方法总是会尝试锁定双方对象:
s.isdisjoint(other) # 锁定双方
s.issubset(other) # 锁定双方
s.issuperset(other) # 锁定双方
涉及多线程访问,以及迭代的操作肯定是非原子化的:
# 非原子化的:检查 - 执行
if elem in s:
s.remove(elem)
# 非线程安全的:在修改期间迭代
for elem in s:
process(elem) # 另一个线程可能会修改 s
当在线程间共享 set 实例时可考虑进行外部同步。 详情参见 Python 对自由线程的支持。
bytearray 对象的线程安全性¶
Concatenation and comparisons use the buffer protocol, which prevents resizing but does not hold the per-object lock. These operations may observe intermediate states from concurrent modifications:
ba + other # may observe concurrent writes ba == other # may observe concurrent writes ba < other # may observe concurrent writes从这里开始的所有其他操作都会持有每对象锁。All other operations from here on hold the per-object lock.
Reading a single element or slice is safe to call from multiple threads:
ba[i] # bytearray.__getitem__ ba[i:j] # 切片下列操作从多个线程调用是安全的并且不会破坏字节数组:
ba[i] = x # 写入单个字节 ba[i:j] = values # 写入切片 ba.append(x) # 添加单个字节 ba.extend(other) # 用可迭代对象扩展 ba.insert(i, x) # 插入单个字节 ba.pop() # 移除并返回末尾字节 ba.pop(i) # 移除并返回指定位置的字节 ba.remove(x) # 移除第首次出现的值 ba.reverse() # 原地反转 ba.clear() # 移除所有字节当 values 为
bytearray时切片赋值会将两个对象都锁定:ba[i:j] = other_bytearray # 两者都将被锁定下列操作会返回新的对象并在执行期间拥有每对象锁:
ba.copy() # 返回一个浅拷贝 ba * n # 对新的 bytearray 执行重复成员检测会在执行期间持有锁:
x in ba # bytearray.__contains__所有其他 bytearray 方法 (如
find(),replace(),split(),decode()等) 会在其执行期间持有每对象锁。涉及多线程访问,以及迭代的操作肯定是非原子化的:
# 非原子化的:检查-执行 if x in ba: ba.remove(x) # 非线程安全的:在修改期间迭代 for byte in ba: process(byte) # 另一个线程可能会修改 ba要安全地迭代可能会被另一个线程修改的 bytearray,可以迭代其拷贝:
# 创建一个拷贝以安全地迭代 for byte in ba.copy(): process(byte)在跨线程共享
bytearray实例时可考虑进行外部同步化。 详情参见 Python 对自由线程的支持。
memoryview 对象的线程安全性¶
memoryview 对象提供对下层对象内部数据的访问而不会拷贝它。 线程安全性同时依赖于 memoryview 本身和下层的缓冲区导出器。
The memoryview implementation uses atomic operations to track its own
exports in the free-threaded build. Creating and
releasing a memoryview are thread-safe. Attribute access (e.g.,
shape, format) reads fields that
are immutable for the lifetime of the memoryview, so concurrent reads
are safe as long as the memoryview has not been released.
However, the actual data accessed through the memoryview is owned by the underlying object. Concurrent access to this data is only safe if the underlying object supports it:
For immutable objects like
bytes, concurrent reads through multiple memoryviews are safe.For mutable objects like
bytearray, reading and writing the same memory region from multiple threads without external synchronization is not safe and may result in data corruption. Note that even read-only memoryviews of mutable objects do not prevent data races if the underlying object is modified from another thread.
# NOT safe: concurrent writes to the same buffer
data = bytearray(1000)
view = memoryview(data)
# Thread 1: view[0:500] = b'x' * 500
# Thread 2: view[0:500] = b'y' * 500
# Safe: use a lock for concurrent access
import threading
lock = threading.Lock()
data = bytearray(1000)
view = memoryview(data)
with lock:
view[0:500] = b'x' * 500
Resizing or reallocating the underlying object (such as calling
bytearray.resize()) while a memoryview is exported raises
BufferError. This is enforced regardless of threading.