Python字典的线程安全性

142

我有一个类,其中包含一个字典

class OrderBook:
    orders = {'Restaurant1': None,
              'Restaurant2': None,
              'Restaurant3': None,
              'Restaurant4': None}

    @staticmethod
    def addOrder(restaurant_name, orders):
        OrderBook.orders[restaurant_name] = orders

我在运行4个线程(每个餐厅一个线程),调用方法OrderBook.addOrder。以下是每个线程运行的函数:

def addOrders(restaurant_name):

    #creates orders
    ...

    OrderBook.addOrder(restaurant_name, orders)

这样做安全吗,还是在调用 addOrder之前必须使用锁?


5
既然每个线程都写入不同的键,那么怎么可能出现问题呢? - Jochen Ritzel
102
根据字典实现的方式不同,可能会出现很多问题。这是一个非常合理的问题。 - Ned Batchelder
5个回答

130

8
这里是effbot.org的why-to/how-to关于实现锁定 - hobs
1
应该考虑单一操作与复合操作,比如获取-添加-设置 - andy
6
问题是,当我频繁地阅读/编写那个词典时,我的内心平静就会付出很大的代价。 - Shihab Shahriar Khan
5
这里的锁几乎不会增加任何开销。为什么呢? - max
2
链接已失效,请在此处查看:https://titanwolf.org/Network/Articles/Article?AID=41de1eb2-63d6-48f3-90a7-914940fbae81#gsc.tab=0 - Ruli
对于像列表这样的数据结构进行多次添加操作会怎样呢?当列表自动调整大小时,底层容器的引用变化是否会导致任何问题? - Zack Light

41

47
这不是Python的一个特性,而是cpython的一个特性。 - phihag
9
没错,根据我的理解,在Jython和IronPython中,内置函数即使在没有使用GIL的情况下也是线程安全的(而且如果它曾经出现,unladen swallow也打算取消GIL)。我认为他没有指定他使用的解释器,所以我默认他是指在CPython中。 - user626998
3
在Jython的情况下正确的方式是:http://www.jython.org/jythonbook/en/1.0/Concurrency.html#java-or-python-apis。 - Evgeni Sergeev

31

谷歌的样式指南建议不要依赖字典的原子性

这在 Is Python variable assignment atomic? 中有更详细的解释。

不要依赖内置类型的原子性。

虽然Python的内置数据类型(如字典)似乎具有原子操作,但在某些情况下它们并不是原子的(例如,如果实现了 __hash____eq__ 作为Python方法),不能依赖其原子性。也不应该依赖原子变量赋值(因为这又依赖于字典)。

使用 Queue 模块的队列数据类型作为线程之间通信的首选方式。否则,使用 threading 模块及其锁定原语。学习条件变量的正确使用方式,以便使用 threading.Condition 而不是使用底层的锁。


我同意这一点:在CPython中已经有了GIL,因此使用锁的性能损失将是可以忽略不计的。当这些CPython实现细节有一天改变时,花费的时间在复杂的代码库中进行错误调试将更加昂贵。


有没有大概的估计锁开销是多少。这些 CPython 的细节不像是每天都在改变(如果有的话)。 - MrR

2

当使用Python内置的dict时,set和get是原子性的(因为cpython的GIL)。然而,似乎这是一种不好的做法,因为诸如.items之类的操作并不是原子性的。

注意 - 如果多个线程正在处理相同的dict键,则get-add-set操作不是线程安全的。


你介意添加一些来源吗?我想阅读一下,特别是你的“注释”。 - lionbigcat

-3

dict() 不是线程安全的。只需要执行以下代码就会看到运行时错误。

import threading

dic = dict()

def producer():
    for i in range(1000000):
        dic[i] = i

def consumer():
    for key, value in dic.items():
        print(key, value)

th1 = threading.Thread(target=producer)
th2 = threading.Thread(target=consumer)
th1.start()
th2.start()
th1.join()
th2.join()

运行时错误:迭代期间字典大小发生了变化


1
这与线程安全无关。您可以使dict的迭代器无效并使用单个线程获得相同的错误:for key in (d := {1: 2, 3: 4}): del d[key] - Expurple

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接