我想防止每个元素同时访问字典。具体来说,我有一个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
:如果线程A
和B
都请求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
同时访问。
有人知道如何实现这一目标吗,也许可以使用信号量之类的其他原语?
答案 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
。