Python 列表切片线程安全吗?
Is Python list slicing thread safe?
来自 Are lists thread-safe?, I need to know if specifically list slicing is thread safe. It's not clear to me from the linked article What kinds of global value mutation are thread-safe?.
根据对 is list.pop thread safe in python 的回答,看来我真的需要知道列表切片是否是一个 原子操作 。
我有一个列表 some_list
,另一个线程正在不断地 append
访问它。在主线程中,我有时会偷看(没有 pop
ing):
- 列表的最后一个元素如下所示:
some_list[-1]
(索引)
- 列表的最后几个元素如下所示:
some_list[-3:]
(切片)
所以,问题是,在 CPython(我使用 Python 3.10)中,列表切片是线程安全的吗?
tl;dr 索引和切片都是线程安全的,但切片由构建切片和应用切片。他们之间线程可以修改我们的列表。
在您发布的 Whosebug 页面中,您可以找到一个 list of atomic operations 可迭代对象,索引和切片都在那里。
让我们看一下用于切片的字节码:
import dis
l = [1, 2, 3]
def f():
l[1:3]
dis.dis(f)
7 0 LOAD_GLOBAL 0 (l)
2 LOAD_CONST 1 (1)
4 LOAD_CONST 2 (3)
6 BUILD_SLICE 2
8 BINARY_SUBSCR
(...)
BINARY_SUBSCR是访问切片的时刻,确实是单次操作。检查:
import time
import threading
l = [0] * 1000000000
def append():
start = time.perf_counter()
time.sleep(0.1)
l.append(1) # Doesn't execute until main thread access slice
print("append", time.perf_counter() - start) # 3.4184917740058154
t = threading.Thread(target=append)
t.start()
start = time.perf_counter()
x = l[:] # Reading via slicing
print("main", time.perf_counter() - start) # 3.418436762993224
print("last_elem", x[-1]) # 0
t.join()
所以当访问一个切片时(和访问一个索引一样)不要担心列表的变化,因为它是简单的操作码并且是受 GIL 保护。
那会发生什么?
# l = [1, 2, 3]
x = l[0:3]
您不能假设 x 将包含所有列表。
- 我们首先 (BUILD_SLICE) 从 0 开始,到 3 结束。
- 然后另一个线程可以修改列表,例如通过追加元素。
- 只有这样我们才能以线程安全方式访问切片。
它会使我们的程序崩溃吗?我怀疑,因为使用越界切片是安全的。
来自 Are lists thread-safe?, I need to know if specifically list slicing is thread safe. It's not clear to me from the linked article What kinds of global value mutation are thread-safe?.
根据对 is list.pop thread safe in python 的回答,看来我真的需要知道列表切片是否是一个 原子操作 。
我有一个列表 some_list
,另一个线程正在不断地 append
访问它。在主线程中,我有时会偷看(没有 pop
ing):
- 列表的最后一个元素如下所示:
some_list[-1]
(索引) - 列表的最后几个元素如下所示:
some_list[-3:]
(切片)
所以,问题是,在 CPython(我使用 Python 3.10)中,列表切片是线程安全的吗?
tl;dr 索引和切片都是线程安全的,但切片由构建切片和应用切片。他们之间线程可以修改我们的列表。
在您发布的 Whosebug 页面中,您可以找到一个 list of atomic operations 可迭代对象,索引和切片都在那里。
让我们看一下用于切片的字节码:
import dis
l = [1, 2, 3]
def f():
l[1:3]
dis.dis(f)
7 0 LOAD_GLOBAL 0 (l)
2 LOAD_CONST 1 (1)
4 LOAD_CONST 2 (3)
6 BUILD_SLICE 2
8 BINARY_SUBSCR
(...)
BINARY_SUBSCR是访问切片的时刻,确实是单次操作。检查:
import time
import threading
l = [0] * 1000000000
def append():
start = time.perf_counter()
time.sleep(0.1)
l.append(1) # Doesn't execute until main thread access slice
print("append", time.perf_counter() - start) # 3.4184917740058154
t = threading.Thread(target=append)
t.start()
start = time.perf_counter()
x = l[:] # Reading via slicing
print("main", time.perf_counter() - start) # 3.418436762993224
print("last_elem", x[-1]) # 0
t.join()
所以当访问一个切片时(和访问一个索引一样)不要担心列表的变化,因为它是简单的操作码并且是受 GIL 保护。
那会发生什么?
# l = [1, 2, 3]
x = l[0:3]
您不能假设 x 将包含所有列表。
- 我们首先 (BUILD_SLICE) 从 0 开始,到 3 结束。
- 然后另一个线程可以修改列表,例如通过追加元素。
- 只有这样我们才能以线程安全方式访问切片。
它会使我们的程序崩溃吗?我怀疑,因为使用越界切片是安全的。