Skip to content

Commit 8583991

Browse files
Pull in main
2 parents c9f1273 + 9bba803 commit 8583991

19 files changed

+180
-130
lines changed

Doc/c-api/init.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,7 @@ Process-wide parameters
818818
.. deprecated:: 3.11
819819
820820
821-
.. c:function:: w_char* Py_GetPythonHome()
821+
.. c:function:: wchar_t* Py_GetPythonHome()
822822
823823
Return the default "home", that is, the value set by a previous call to
824824
:c:func:`Py_SetPythonHome`, or the value of the :envvar:`PYTHONHOME`

Doc/library/functools.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ The :mod:`functools` module defines the following functions:
8686
The cached value can be cleared by deleting the attribute. This
8787
allows the *cached_property* method to run again.
8888

89+
The *cached_property* does not prevent a possible race condition in
90+
multi-threaded usage. The getter function could run more than once on the
91+
same instance, with the latest run setting the cached value. If the cached
92+
property is idempotent or otherwise not harmful to run more than once on an
93+
instance, this is fine. If synchronization is needed, implement the necessary
94+
locking inside the decorated getter function or around the cached property
95+
access.
96+
8997
Note, this decorator interferes with the operation of :pep:`412`
9098
key-sharing dictionaries. This means that instance dictionaries
9199
can take more space than usual.
@@ -110,6 +118,14 @@ The :mod:`functools` module defines the following functions:
110118
def stdev(self):
111119
return statistics.stdev(self._data)
112120

121+
122+
.. versionchanged:: 3.12
123+
Prior to Python 3.12, ``cached_property`` included an undocumented lock to
124+
ensure that in multi-threaded usage the getter function was guaranteed to
125+
run only once per instance. However, the lock was per-property, not
126+
per-instance, which could result in unacceptably high lock contention. In
127+
Python 3.12+ this locking is removed.
128+
113129
.. versionadded:: 3.8
114130

115131

Doc/library/struct.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ ordering::
371371
>>> from struct import *
372372
>>> pack(">bhl", 1, 2, 3)
373373
b'\x01\x00\x02\x00\x00\x00\x03'
374-
>>> unpack('>bhl', b'\x01\x00\x02\x00\x00\x00\x03'
374+
>>> unpack('>bhl', b'\x01\x00\x02\x00\x00\x00\x03')
375375
(1, 2, 3)
376376
>>> calcsize('>bhl')
377377
7

Doc/whatsnew/3.12.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,15 @@ Changes in the Python API
761761
around process-global resources, which are best managed from the main interpreter.
762762
(Contributed by Dong-hee Na in :gh:`99127`.)
763763

764+
* The undocumented locking behavior of :func:`~functools.cached_property`
765+
is removed, because it locked across all instances of the class, leading to high
766+
lock contention. This means that a cached property getter function could now run
767+
more than once for a single instance, if two threads race. For most simple
768+
cached properties (e.g. those that are idempotent and simply calculate a value
769+
based on other attributes of the instance) this will be fine. If
770+
synchronization is needed, implement locking within the cached property getter
771+
function or around multi-threaded access points.
772+
764773

765774
Build Changes
766775
=============
@@ -861,6 +870,19 @@ New Features
861870
get a frame variable by its name.
862871
(Contributed by Victor Stinner in :gh:`91248`.)
863872

873+
* Add :c:func:`PyErr_GetRaisedException` and :c:func:`PyErr_SetRaisedException`
874+
for saving and restoring the current exception.
875+
These functions return and accept a single exception object,
876+
rather than the triple arguments of the now-deprecated
877+
:c:func:`PyErr_Fetch` and :c:func:`PyErr_Restore`.
878+
This is less error prone and a bit more efficient.
879+
(Contributed by Mark Shannon in :gh:`101578`.)
880+
881+
* Add :c:func:`PyException_GetArgs` and :c:func:`PyException_SetArgs`
882+
as convenience functions for retrieving and modifying
883+
the :attr:`~BaseException.args` passed to the exception's constructor.
884+
(Contributed by Mark Shannon in :gh:`101578`.)
885+
864886
Porting to Python 3.12
865887
----------------------
866888

@@ -984,6 +1006,11 @@ Deprecated
9841006
(Contributed in :gh:`47146` by Petr Viktorin, based on
9851007
earlier work by Alexander Belopolsky and Matthias Braun.)
9861008

1009+
* :c:func:`PyErr_Fetch` and :c:func:`PyErr_Restore` are deprecated.
1010+
Use :c:func:`PyErr_GetRaisedException` and
1011+
:c:func:`PyErr_SetRaisedException` instead.
1012+
(Contributed by Mark Shannon in :gh:`101578`.)
1013+
9871014

9881015
Removed
9891016
-------

Include/cpython/genobject.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ extern "C" {
1313
and coroutine objects. */
1414
#define _PyGenObject_HEAD(prefix) \
1515
PyObject_HEAD \
16-
/* The code object backing the generator */ \
17-
PyCodeObject *prefix##_code; \
1816
/* List of weak reference. */ \
1917
PyObject *prefix##_weakreflist; \
2018
/* Name of the generator. */ \
@@ -28,7 +26,7 @@ extern "C" {
2826
char prefix##_running_async; \
2927
/* The frame */ \
3028
int8_t prefix##_frame_state; \
31-
PyObject *prefix##_iframe[1];
29+
PyObject *prefix##_iframe[1]; \
3230

3331
typedef struct {
3432
/* The gi_ prefix is intended to remind of generator-iterator. */
@@ -46,6 +44,7 @@ PyAPI_FUNC(PyObject *) PyGen_NewWithQualName(PyFrameObject *,
4644
PyAPI_FUNC(int) _PyGen_SetStopIterationValue(PyObject *);
4745
PyAPI_FUNC(int) _PyGen_FetchStopIterationValue(PyObject **);
4846
PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self);
47+
PyAPI_FUNC(PyCodeObject *) PyGen_GetCode(PyGenObject *gen);
4948

5049

5150
/* --- PyCoroObject ------------------------------------------------------- */

Include/internal/pycore_frame.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ _PyFrame_GetFrameObject(_PyInterpreterFrame *frame)
209209
* frames like the ones in generators and coroutines.
210210
*/
211211
void
212-
_PyFrame_Clear(_PyInterpreterFrame * frame);
212+
_PyFrame_ClearExceptCode(_PyInterpreterFrame * frame);
213213

214214
int
215215
_PyFrame_Traverse(_PyInterpreterFrame *frame, visitproc visit, void *arg);

Lib/functools.py

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -959,15 +959,12 @@ def __isabstractmethod__(self):
959959
### cached_property() - computed once per instance, cached as attribute
960960
################################################################################
961961

962-
_NOT_FOUND = object()
963-
964962

965963
class cached_property:
966964
def __init__(self, func):
967965
self.func = func
968966
self.attrname = None
969967
self.__doc__ = func.__doc__
970-
self.lock = RLock()
971968

972969
def __set_name__(self, owner, name):
973970
if self.attrname is None:
@@ -992,21 +989,15 @@ def __get__(self, instance, owner=None):
992989
f"instance to cache {self.attrname!r} property."
993990
)
994991
raise TypeError(msg) from None
995-
val = cache.get(self.attrname, _NOT_FOUND)
996-
if val is _NOT_FOUND:
997-
with self.lock:
998-
# check if another thread filled cache while we awaited lock
999-
val = cache.get(self.attrname, _NOT_FOUND)
1000-
if val is _NOT_FOUND:
1001-
val = self.func(instance)
1002-
try:
1003-
cache[self.attrname] = val
1004-
except TypeError:
1005-
msg = (
1006-
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
1007-
f"does not support item assignment for caching {self.attrname!r} property."
1008-
)
1009-
raise TypeError(msg) from None
992+
val = self.func(instance)
993+
try:
994+
cache[self.attrname] = val
995+
except TypeError:
996+
msg = (
997+
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
998+
f"does not support item assignment for caching {self.attrname!r} property."
999+
)
1000+
raise TypeError(msg) from None
10101001
return val
10111002

10121003
__class_getitem__ = classmethod(GenericAlias)

Lib/test/test_capi/test_misc.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,11 @@ def test_pendingcalls_non_threaded(self):
12131213
self.pendingcalls_submit(l, n)
12141214
self.pendingcalls_wait(l, n)
12151215

1216+
def test_gen_get_code(self):
1217+
def genf(): yield
1218+
gen = genf()
1219+
self.assertEqual(_testcapi.gen_get_code(gen), gen.gi_code)
1220+
12161221

12171222
class SubinterpreterTest(unittest.TestCase):
12181223

Lib/test/test_functools.py

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2931,21 +2931,6 @@ def get_cost(self):
29312931
cached_cost = py_functools.cached_property(get_cost)
29322932

29332933

2934-
class CachedCostItemWait:
2935-
2936-
def __init__(self, event):
2937-
self._cost = 1
2938-
self.lock = py_functools.RLock()
2939-
self.event = event
2940-
2941-
@py_functools.cached_property
2942-
def cost(self):
2943-
self.event.wait(1)
2944-
with self.lock:
2945-
self._cost += 1
2946-
return self._cost
2947-
2948-
29492934
class CachedCostItemWithSlots:
29502935
__slots__ = ('_cost')
29512936

@@ -2970,27 +2955,6 @@ def test_cached_attribute_name_differs_from_func_name(self):
29702955
self.assertEqual(item.get_cost(), 4)
29712956
self.assertEqual(item.cached_cost, 3)
29722957

2973-
@threading_helper.requires_working_threading()
2974-
def test_threaded(self):
2975-
go = threading.Event()
2976-
item = CachedCostItemWait(go)
2977-
2978-
num_threads = 3
2979-
2980-
orig_si = sys.getswitchinterval()
2981-
sys.setswitchinterval(1e-6)
2982-
try:
2983-
threads = [
2984-
threading.Thread(target=lambda: item.cost)
2985-
for k in range(num_threads)
2986-
]
2987-
with threading_helper.start_threads(threads):
2988-
go.set()
2989-
finally:
2990-
sys.setswitchinterval(orig_si)
2991-
2992-
self.assertEqual(item.cost, 2)
2993-
29942958
def test_object_with_slots(self):
29952959
item = CachedCostItemWithSlots()
29962960
with self.assertRaisesRegex(

Lib/test/test_sys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1460,7 +1460,7 @@ def bar(cls):
14601460
check(bar, size('PP'))
14611461
# generator
14621462
def get_gen(): yield 1
1463-
check(get_gen(), size('P2P4P4c7P2ic??2P'))
1463+
check(get_gen(), size('PP4P4c7P2ic??2P'))
14641464
# iterator
14651465
check(iter('abc'), size('lP'))
14661466
# callable-iterator

0 commit comments

Comments
 (0)