我是否需要锁定块来迭代共享列表?

Pha*_*ate 6 python

我知道python列表是线程安全的.但是,假设我有一个共享列表

object_list = []
Run Code Online (Sandbox Code Playgroud)

现在,线程定期向此列表添加一个对象(数据结构),

object_list.add(aNewObject)
Run Code Online (Sandbox Code Playgroud)

调用时,另一个线程在for循环中访问此列表

for i in object_list:
    #do something with i
Run Code Online (Sandbox Code Playgroud)

我需要这样的东西:

myLock.acquire()
either add an element or access the list with the for
myLock.release()
Run Code Online (Sandbox Code Playgroud)

或者我还好吗?我不关心错过添加,但我担心如果在读取线程在列表中间迭代时添加新元素会发生什么...

Bak*_*riu 6

这取决于您期望的保证。首先,Python语言不保证任何线程安全。

由于其 GIL ,CPython 实现顺便提供了一些线程安全性,但如果您计划在任何其他 Python 实现下运行代码,则每次访问资源时都应该在各处使用锁。

现在,对于 CPython 的特定考虑,如果您知道代码将仅通过append或添加元素extend,那么可能发生的最坏情况是for循环也会迭代新元素。这可能是也可能不是您想要的1

>>> a = [0]
>>> for i, el in enumerate(a):
...     if i % 2 == 0:
...             a.append(i)
...     print(i, el)
... 
0 0
1 0
>>> a
[0, 0]
>>> for i, el in enumerate(a):
...     if i % 2 == 0:
...             a.append(i)
...     print(i, el)
... 
0 0
1 0
2 0
3 2
>>> a
[0, 0, 0, 2]
Run Code Online (Sandbox Code Playgroud)

如果使用该方法添加新项目insert,则可能会多次迭代某些值,而另一些值则可能不会在迭代期间迭代。for. 例如:

>>> a = [1]
>>> for i,el in enumerate(a):
...     if i % 2 == 0:
...             a.insert(0, i)
...     print(i, el)
... 
(0, 1)
(1, 1)
>>> a
[0, 1]
Run Code Online (Sandbox Code Playgroud)

这里1迭代了两次并且新插入了0在 中从未出现过loop

从列表中删除元素也可能会导致类似的行为,其中某些元素会被跳过:

>>> for i, el in enumerate(a):
...     if i % 2 == 0:
...             a.remove(i)
...     print(i, el)
... 
(0, 0)
>>> a
[1]
Run Code Online (Sandbox Code Playgroud)

正如你所看到的1在循环期间从未见过。

这就是说,在迭代期间改变序列通常是您不希望发生的事情。对于你的情况,我建议使用锁。请注意,锁是上下文管理器,因此我将循环编写为:

with list_lock:
    for element in object_list:
        # do stuff
Run Code Online (Sandbox Code Playgroud)

这简化了代码,因为您不必记住显式释放锁。插入元素时也应该这样做(否则没用):

with list_lock:
    object_list.append(new_element)
Run Code Online (Sandbox Code Playgroud)

1我不使用单独的线程来简化示例并使其易于重现。在 CPython 中,在多个线程中完成操作不应改变您看到的行为。在其他实现中情况并非如此。显然,使用多个线程来重现该行为要困难得多。