没有WeakValueDictionary的Python行锁定

时间:2018-07-31 11:23:32

标签: python multithreading

我想防止每个元素同时访问字典。具体来说,我有一个Cache类:

class Cache:
    def __init__(self):
        self._values = {}
    def query(self, item):
        try:
            return self._values[item]
        except KeyError:
            value = compute_value(item) # Expensive operation
            self._values[item] = value
            return value

换句话说,Cache应该根据需要计算项目值,然后将其缓存以供以后查询。

Cache用于多个线程。我想避免线程为同一compute_value(...)同时调用item:如果线程AB都请求my_item的值,则只有{{1 }}应该对其进行计算。 A应该等待其结果,然后使用缓存的值。

我实现了以下步骤:

B

这有效。特别是,我对from threading import Lock from weakref import WeakValueDictionary class Cache: def __init__(self): self._values = {} self._locks = WeakValueDictionary() def query(self, item): with self._locks.setdefault(item, Lock()): try: return self._values[item] except KeyError: value = compute_value(item) self._values[item] = value return value 的使用可确保对同一WeakValueDictionary的并发查询获得相同的item,但这些锁不会永远留在内存中。

问题是我的应用程序实际上在运行中创建了Lock的许多实例。因此,呼叫Cache成为性能瓶颈。

我正在寻找一种解决方案,该解决方案可以使我完成相同的任务,但是使用普通的Python字典而不是self._values = WeakValueDictionary()。我尝试过:

WeakValueDictionary

但是,这并不能完全阻止并发访问。具体来说,如果线程class Cache: def __init__(self): self._values = {} self._locks = {} # No more WeakValueDictionary def query(self, item): with self._locks.setdefault(item, Lock()): # as before... del self._locks[item] A获得了相同的锁,而B删除了它,而A仍然持有它,那么另一个线程B可以进入并获得同一行的 new 锁,因此可以与C同时访问。

有人知道如何实现这一目标吗,也许可以使用信号量之类的其他原语?

2 个答案:

答案 0 :(得分:1)

我真的无法想象自动同步的方法,因此我将使用主锁保护锁访问和未决请求的计数。仅当没有其他请求挂起(计数== 0)时,才应删除项目锁。代码较大,但应该是防弹的:

class Cache:
    def __init__(self):
        self._values = {}
        self._locks = {}
        self._master_lock = Lock()
    def query(self, item):
        with self._master_lock:
            if item in self._values:         # if value is ready return it immediately
                return self._values[item]
            lock = self._locks.setdefault(   # else build or use an item lock
                item, [Lock(), 0])           # and say we are pending on it
            lock[1] += 1
        with lock[0]:                        # release master lock and acquire item one
            exc = None                       # be prepared to any exception
            try:                             # read or compute (first time only) the value
                val = self._values.setdefault(
                    item, compute_value(item))
            except Exception as e:
                exc = e                      # note the exception for later re-raise
        with self._master_lock:              # release item lock and take again master one
            lock[1] -= 1                     # we are no longer pending
            if lock[1] == 0:                 # if no other thread is either
                del self._locks[item]        # delete the item lock
        if exc:
            raise exc                        # eventually re-raise
        return val

答案 1 :(得分:0)

您如何确定性能瓶颈是self._values = WeakValueDictionary()?这没有道理。我做了一个个人资料:

%timeit locks = WeakValueDictionary()

2.52 µs ± 50.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

如您所见,每个循环仅花费2.52 µs。您需要创建数百万个Cache对象,而只需花费两秒钟的时间。

据我所知,您的性能瓶颈应该来自with self._locks.setdefault(item, Lock()):,因为它将为每个并发查询创建新的Lock对象。

实际上有一件事我无法完全理解,为什么您需要在查询后删除Lock对象?难道不是每个key都有一个相应的Lock吗?并且,当您删除此key时,您也可以删除其Lock