mirror of https://github.com/python/cpython
gh-87634: remove locking from functools.cached_property (GH-101890)
Remove the undocumented locking capabilities of functools.cached_property.
This commit is contained in:
parent
8f647477f0
commit
056dfc71dc
|
@ -86,6 +86,14 @@ The :mod:`functools` module defines the following functions:
|
|||
The cached value can be cleared by deleting the attribute. This
|
||||
allows the *cached_property* method to run again.
|
||||
|
||||
The *cached_property* does not prevent a possible race condition in
|
||||
multi-threaded usage. The getter function could run more than once on the
|
||||
same instance, with the latest run setting the cached value. If the cached
|
||||
property is idempotent or otherwise not harmful to run more than once on an
|
||||
instance, this is fine. If synchronization is needed, implement the necessary
|
||||
locking inside the decorated getter function or around the cached property
|
||||
access.
|
||||
|
||||
Note, this decorator interferes with the operation of :pep:`412`
|
||||
key-sharing dictionaries. This means that instance dictionaries
|
||||
can take more space than usual.
|
||||
|
@ -110,6 +118,14 @@ The :mod:`functools` module defines the following functions:
|
|||
def stdev(self):
|
||||
return statistics.stdev(self._data)
|
||||
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
Prior to Python 3.12, ``cached_property`` included an undocumented lock to
|
||||
ensure that in multi-threaded usage the getter function was guaranteed to
|
||||
run only once per instance. However, the lock was per-property, not
|
||||
per-instance, which could result in unacceptably high lock contention. In
|
||||
Python 3.12+ this locking is removed.
|
||||
|
||||
.. versionadded:: 3.8
|
||||
|
||||
|
||||
|
|
|
@ -761,6 +761,15 @@ Changes in the Python API
|
|||
around process-global resources, which are best managed from the main interpreter.
|
||||
(Contributed by Dong-hee Na in :gh:`99127`.)
|
||||
|
||||
* The undocumented locking behavior of :func:`~functools.cached_property`
|
||||
is removed, because it locked across all instances of the class, leading to high
|
||||
lock contention. This means that a cached property getter function could now run
|
||||
more than once for a single instance, if two threads race. For most simple
|
||||
cached properties (e.g. those that are idempotent and simply calculate a value
|
||||
based on other attributes of the instance) this will be fine. If
|
||||
synchronization is needed, implement locking within the cached property getter
|
||||
function or around multi-threaded access points.
|
||||
|
||||
|
||||
Build Changes
|
||||
=============
|
||||
|
|
|
@ -959,15 +959,12 @@ class singledispatchmethod:
|
|||
### cached_property() - computed once per instance, cached as attribute
|
||||
################################################################################
|
||||
|
||||
_NOT_FOUND = object()
|
||||
|
||||
|
||||
class cached_property:
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
self.attrname = None
|
||||
self.__doc__ = func.__doc__
|
||||
self.lock = RLock()
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
if self.attrname is None:
|
||||
|
@ -992,21 +989,15 @@ class cached_property:
|
|||
f"instance to cache {self.attrname!r} property."
|
||||
)
|
||||
raise TypeError(msg) from None
|
||||
val = cache.get(self.attrname, _NOT_FOUND)
|
||||
if val is _NOT_FOUND:
|
||||
with self.lock:
|
||||
# check if another thread filled cache while we awaited lock
|
||||
val = cache.get(self.attrname, _NOT_FOUND)
|
||||
if val is _NOT_FOUND:
|
||||
val = self.func(instance)
|
||||
try:
|
||||
cache[self.attrname] = val
|
||||
except TypeError:
|
||||
msg = (
|
||||
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
|
||||
f"does not support item assignment for caching {self.attrname!r} property."
|
||||
)
|
||||
raise TypeError(msg) from None
|
||||
val = self.func(instance)
|
||||
try:
|
||||
cache[self.attrname] = val
|
||||
except TypeError:
|
||||
msg = (
|
||||
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
|
||||
f"does not support item assignment for caching {self.attrname!r} property."
|
||||
)
|
||||
raise TypeError(msg) from None
|
||||
return val
|
||||
|
||||
__class_getitem__ = classmethod(GenericAlias)
|
||||
|
|
|
@ -2931,21 +2931,6 @@ class OptionallyCachedCostItem:
|
|||
cached_cost = py_functools.cached_property(get_cost)
|
||||
|
||||
|
||||
class CachedCostItemWait:
|
||||
|
||||
def __init__(self, event):
|
||||
self._cost = 1
|
||||
self.lock = py_functools.RLock()
|
||||
self.event = event
|
||||
|
||||
@py_functools.cached_property
|
||||
def cost(self):
|
||||
self.event.wait(1)
|
||||
with self.lock:
|
||||
self._cost += 1
|
||||
return self._cost
|
||||
|
||||
|
||||
class CachedCostItemWithSlots:
|
||||
__slots__ = ('_cost')
|
||||
|
||||
|
@ -2970,27 +2955,6 @@ class TestCachedProperty(unittest.TestCase):
|
|||
self.assertEqual(item.get_cost(), 4)
|
||||
self.assertEqual(item.cached_cost, 3)
|
||||
|
||||
@threading_helper.requires_working_threading()
|
||||
def test_threaded(self):
|
||||
go = threading.Event()
|
||||
item = CachedCostItemWait(go)
|
||||
|
||||
num_threads = 3
|
||||
|
||||
orig_si = sys.getswitchinterval()
|
||||
sys.setswitchinterval(1e-6)
|
||||
try:
|
||||
threads = [
|
||||
threading.Thread(target=lambda: item.cost)
|
||||
for k in range(num_threads)
|
||||
]
|
||||
with threading_helper.start_threads(threads):
|
||||
go.set()
|
||||
finally:
|
||||
sys.setswitchinterval(orig_si)
|
||||
|
||||
self.assertEqual(item.cost, 2)
|
||||
|
||||
def test_object_with_slots(self):
|
||||
item = CachedCostItemWithSlots()
|
||||
with self.assertRaisesRegex(
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Remove locking behavior from :func:`functools.cached_property`.
|
Loading…
Reference in New Issue