mirror of
https://github.com/python/cpython
synced 2024-09-20 23:51:52 +00:00
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
|
The cached value can be cleared by deleting the attribute. This
|
||||||
allows the *cached_property* method to run again.
|
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`
|
Note, this decorator interferes with the operation of :pep:`412`
|
||||||
key-sharing dictionaries. This means that instance dictionaries
|
key-sharing dictionaries. This means that instance dictionaries
|
||||||
can take more space than usual.
|
can take more space than usual.
|
||||||
|
@ -110,6 +118,14 @@ The :mod:`functools` module defines the following functions:
|
||||||
def stdev(self):
|
def stdev(self):
|
||||||
return statistics.stdev(self._data)
|
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
|
.. versionadded:: 3.8
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -761,6 +761,15 @@ Changes in the Python API
|
||||||
around process-global resources, which are best managed from the main interpreter.
|
around process-global resources, which are best managed from the main interpreter.
|
||||||
(Contributed by Dong-hee Na in :gh:`99127`.)
|
(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
|
Build Changes
|
||||||
=============
|
=============
|
||||||
|
|
|
@ -959,15 +959,12 @@ def __isabstractmethod__(self):
|
||||||
### cached_property() - computed once per instance, cached as attribute
|
### cached_property() - computed once per instance, cached as attribute
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
_NOT_FOUND = object()
|
|
||||||
|
|
||||||
|
|
||||||
class cached_property:
|
class cached_property:
|
||||||
def __init__(self, func):
|
def __init__(self, func):
|
||||||
self.func = func
|
self.func = func
|
||||||
self.attrname = None
|
self.attrname = None
|
||||||
self.__doc__ = func.__doc__
|
self.__doc__ = func.__doc__
|
||||||
self.lock = RLock()
|
|
||||||
|
|
||||||
def __set_name__(self, owner, name):
|
def __set_name__(self, owner, name):
|
||||||
if self.attrname is None:
|
if self.attrname is None:
|
||||||
|
@ -992,12 +989,6 @@ def __get__(self, instance, owner=None):
|
||||||
f"instance to cache {self.attrname!r} property."
|
f"instance to cache {self.attrname!r} property."
|
||||||
)
|
)
|
||||||
raise TypeError(msg) from None
|
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)
|
val = self.func(instance)
|
||||||
try:
|
try:
|
||||||
cache[self.attrname] = val
|
cache[self.attrname] = val
|
||||||
|
|
|
@ -2931,21 +2931,6 @@ def get_cost(self):
|
||||||
cached_cost = py_functools.cached_property(get_cost)
|
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:
|
class CachedCostItemWithSlots:
|
||||||
__slots__ = ('_cost')
|
__slots__ = ('_cost')
|
||||||
|
|
||||||
|
@ -2970,27 +2955,6 @@ def test_cached_attribute_name_differs_from_func_name(self):
|
||||||
self.assertEqual(item.get_cost(), 4)
|
self.assertEqual(item.get_cost(), 4)
|
||||||
self.assertEqual(item.cached_cost, 3)
|
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):
|
def test_object_with_slots(self):
|
||||||
item = CachedCostItemWithSlots()
|
item = CachedCostItemWithSlots()
|
||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Remove locking behavior from :func:`functools.cached_property`.
|
Loading…
Reference in a new issue