From 88be4b3f678dd4762590fdd4d3523a8312a4dca8 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 7 Apr 2026 12:01:42 +0200 Subject: [PATCH 1/3] Expose Py_CriticalSection in Stable ABI --- Doc/c-api/synchronization.rst | 55 +++++++++++++++--- Doc/data/stable_abi.dat | 10 ++++ Include/cpython/critical_section.h | 93 +++--------------------------- Include/critical_section.h | 85 +++++++++++++++++++++++++++ Lib/test/test_cext/extension.c | 4 ++ Lib/test/test_stable_abi_ctypes.py | 4 ++ Misc/stable_abi.toml | 22 +++++++ PC/python3dll.c | 4 ++ 8 files changed, 182 insertions(+), 95 deletions(-) diff --git a/Doc/c-api/synchronization.rst b/Doc/c-api/synchronization.rst index 53c9faeae35464..4df68564941e4b 100644 --- a/Doc/c-api/synchronization.rst +++ b/Doc/c-api/synchronization.rst @@ -84,11 +84,6 @@ there is no :c:type:`PyObject` -- for example, when working with a C type that does not extend or wrap :c:type:`PyObject` but still needs to call into the C API in a manner that might lead to deadlocks. -The functions and structs used by the macros are exposed for cases -where C macros are not available. They should only be used as in the -given macro expansions. Note that the sizes and contents of the structures may -change in future Python versions. - .. note:: Operations that need to lock two objects at once must use @@ -114,12 +109,15 @@ section API avoids potential deadlocks due to reentrancy and lock ordering by allowing the runtime to temporarily suspend the critical section if the code triggered by the finalizer blocks and calls :c:func:`PyEval_SaveThread`. +.. _critical-section-macros: + .. c:macro:: Py_BEGIN_CRITICAL_SECTION(op) Acquires the per-object lock for the object *op* and begins a critical section. - In the free-threaded build, this macro expands to:: + In the free-threaded build, and when building for the + :ref:`Stable ABI `, this macro expands to:: { PyCriticalSection _py_cs; @@ -150,7 +148,8 @@ code triggered by the finalizer blocks and calls :c:func:`PyEval_SaveThread`. Ends the critical section and releases the per-object lock. - In the free-threaded build, this macro expands to:: + In the free-threaded build, and when building for the + :ref:`Stable ABI `, this macro expands to:: PyCriticalSection_End(&_py_cs); } @@ -179,7 +178,8 @@ code triggered by the finalizer blocks and calls :c:func:`PyEval_SaveThread`. Locks the mutexes *m1* and *m2* and begins a critical section. - In the free-threaded build, this macro expands to:: + In the free-threaded build, and when building for the + :ref:`Stable ABI `, this macro expands to:: { PyCriticalSection2 _py_cs2; @@ -196,7 +196,8 @@ code triggered by the finalizer blocks and calls :c:func:`PyEval_SaveThread`. Ends the critical section and releases the per-object locks. - In the free-threaded build, this macro expands to:: + In the free-threaded build, and when building for the + :ref:`Stable ABI `, this macro expands to:: PyCriticalSection2_End(&_py_cs2); } @@ -205,6 +206,42 @@ code triggered by the finalizer blocks and calls :c:func:`PyEval_SaveThread`. .. versionadded:: 3.13 +Low-level critical section API +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following functions and structs are exposed for cases where C macros +are not available. + +.. c:function:: void PyCriticalSection_Begin(PyCriticalSection *c, PyObject *op) + void PyCriticalSection_End(PyCriticalSection *c) + void PyCriticalSection2_Begin(PyCriticalSection2 *c, PyObject *a, PyObject *b) + void PyCriticalSection2_End(PyCriticalSection2 *c); + + To be used only as in the macro expansions + listed :ref:`earlier in this section `. + + .. versionadded:: 3.13 + +.. c:type:: PyCriticalSection + PyCriticalSection2 + + To be used only as in the macro expansions + listed :ref:`earlier in this section `. + Note that the contents of the structures are private and their meaning may + change in future Python versions. + + .. versionadded:: 3.13 + +.. c:function:: void PyCriticalSection_BeginMutex(PyCriticalSection *c, PyMutex *m); + void PyCriticalSection2_BeginMutex(PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2); + + .. (These need to be in a separate section without a Stable ABI anotation.) + + To be used only as in the macro expansions + listed :ref:`earlier in this section `. + + .. versionadded:: 3.14 + Legacy locking APIs ------------------- diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 4ae5e999f0bf21..972ee74b14faad 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -129,6 +129,12 @@ func,PyComplex_FromDoubles,3.2,, func,PyComplex_ImagAsDouble,3.2,, func,PyComplex_RealAsDouble,3.2,, data,PyComplex_Type,3.2,, +type,PyCriticalSection,3.15,,full-abi +type,PyCriticalSection2,3.15,,full-abi +func,PyCriticalSection2_Begin,3.15,, +func,PyCriticalSection2_End,3.15,, +func,PyCriticalSection_Begin,3.15,, +func,PyCriticalSection_End,3.15,, func,PyDescr_NewClassMethod,3.2,, func,PyDescr_NewGetSet,3.2,, func,PyDescr_NewMember,3.2,, @@ -905,6 +911,8 @@ macro,Py_AUDIT_READ,3.12,, func,Py_AddPendingCall,3.2,, func,Py_AtExit,3.2,, macro,Py_BEGIN_ALLOW_THREADS,3.2,, +macro,Py_BEGIN_CRITICAL_SECTION,3.15,, +macro,Py_BEGIN_CRITICAL_SECTION2,3.15,, macro,Py_BLOCK_THREADS,3.2,, func,Py_BuildValue,3.2,, func,Py_BytesMain,3.8,, @@ -912,6 +920,8 @@ func,Py_CompileString,3.2,, func,Py_DecRef,3.2,, func,Py_DecodeLocale,3.7,, macro,Py_END_ALLOW_THREADS,3.2,, +macro,Py_END_CRITICAL_SECTION,3.15,, +macro,Py_END_CRITICAL_SECTION2,3.15,, func,Py_EncodeLocale,3.7,, func,Py_EndInterpreter,3.2,, func,Py_EnterRecursiveCall,3.9,, diff --git a/Include/cpython/critical_section.h b/Include/cpython/critical_section.h index 4fc46fefb93a24..bcba32da412f32 100644 --- a/Include/cpython/critical_section.h +++ b/Include/cpython/critical_section.h @@ -2,15 +2,6 @@ # error "this header file must not be included directly" #endif -// Python critical sections -// -// Conceptually, critical sections are a deadlock avoidance layer on top of -// per-object locks. These helpers, in combination with those locks, replace -// our usage of the global interpreter lock to provide thread-safety for -// otherwise thread-unsafe objects, such as dict. -// -// NOTE: These APIs are no-ops in non-free-threaded builds. -// // Straightforward per-object locking could introduce deadlocks that were not // present when running with the GIL. Threads may hold locks for multiple // objects simultaneously because Python operations can nest. If threads were @@ -43,52 +34,19 @@ // `_PyThreadState_Attach()`, it resumes the top-most (i.e., most recent) // critical section by reacquiring the associated lock or locks. See // `_PyCriticalSection_Resume()`. -// -// NOTE: Only the top-most critical section is guaranteed to be active. -// Operations that need to lock two objects at once must use -// `Py_BEGIN_CRITICAL_SECTION2()`. You *CANNOT* use nested critical sections -// to lock more than one object at once, because the inner critical section -// may suspend the outer critical sections. This API does not provide a way -// to lock more than two objects at once (though it could be added later -// if actually needed). -// -// NOTE: Critical sections implicitly behave like reentrant locks because -// attempting to acquire the same lock will suspend any outer (earlier) -// critical sections. However, they are less efficient for this use case than -// purposefully designed reentrant locks. -// -// Example usage: -// Py_BEGIN_CRITICAL_SECTION(op); -// ... -// Py_END_CRITICAL_SECTION(); -// -// To lock two objects at once: -// Py_BEGIN_CRITICAL_SECTION2(op1, op2); -// ... -// Py_END_CRITICAL_SECTION2(); - -typedef struct PyCriticalSection PyCriticalSection; -typedef struct PyCriticalSection2 PyCriticalSection2; - -PyAPI_FUNC(void) -PyCriticalSection_Begin(PyCriticalSection *c, PyObject *op); PyAPI_FUNC(void) PyCriticalSection_BeginMutex(PyCriticalSection *c, PyMutex *m); -PyAPI_FUNC(void) -PyCriticalSection_End(PyCriticalSection *c); - -PyAPI_FUNC(void) -PyCriticalSection2_Begin(PyCriticalSection2 *c, PyObject *a, PyObject *b); - PyAPI_FUNC(void) PyCriticalSection2_BeginMutex(PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2); -PyAPI_FUNC(void) -PyCriticalSection2_End(PyCriticalSection2 *c); - #ifndef Py_GIL_DISABLED +#undef Py_BEGIN_CRITICAL_SECTION +#undef Py_END_CRITICAL_SECTION +#undef Py_BEGIN_CRITICAL_SECTION2 +#undef Py_END_CRITICAL_SECTION2 + # define Py_BEGIN_CRITICAL_SECTION(op) \ { # define Py_BEGIN_CRITICAL_SECTION_MUTEX(mutex) \ @@ -101,54 +59,17 @@ PyCriticalSection2_End(PyCriticalSection2 *c); { # define Py_END_CRITICAL_SECTION2() \ } -#else /* !Py_GIL_DISABLED */ - -// NOTE: the contents of this struct are private and may change betweeen -// Python releases without a deprecation period. -struct PyCriticalSection { - // Tagged pointer to an outer active critical section (or 0). - uintptr_t _cs_prev; - - // Mutex used to protect critical section - PyMutex *_cs_mutex; -}; - -// A critical section protected by two mutexes. Use -// Py_BEGIN_CRITICAL_SECTION2 and Py_END_CRITICAL_SECTION2. -// NOTE: the contents of this struct are private and may change betweeen -// Python releases without a deprecation period. -struct PyCriticalSection2 { - PyCriticalSection _cs_base; - PyMutex *_cs_mutex2; -}; - -# define Py_BEGIN_CRITICAL_SECTION(op) \ - { \ - PyCriticalSection _py_cs; \ - PyCriticalSection_Begin(&_py_cs, _PyObject_CAST(op)) +#else /* !Py_GIL_DISABLED */ # define Py_BEGIN_CRITICAL_SECTION_MUTEX(mutex) \ { \ PyCriticalSection _py_cs; \ PyCriticalSection_BeginMutex(&_py_cs, mutex) -# define Py_END_CRITICAL_SECTION() \ - PyCriticalSection_End(&_py_cs); \ - } - -# define Py_BEGIN_CRITICAL_SECTION2(a, b) \ - { \ - PyCriticalSection2 _py_cs2; \ - PyCriticalSection2_Begin(&_py_cs2, _PyObject_CAST(a), _PyObject_CAST(b)) - # define Py_BEGIN_CRITICAL_SECTION2_MUTEX(m1, m2) \ { \ PyCriticalSection2 _py_cs2; \ PyCriticalSection2_BeginMutex(&_py_cs2, m1, m2) -# define Py_END_CRITICAL_SECTION2() \ - PyCriticalSection2_End(&_py_cs2); \ - } - -#endif +#endif /* !Py_GIL_DISABLED */ diff --git a/Include/critical_section.h b/Include/critical_section.h index 3b37615a8b17e2..1eabf372c15f67 100644 --- a/Include/critical_section.h +++ b/Include/critical_section.h @@ -4,6 +4,91 @@ extern "C" { #endif +// Python critical sections +// +// Conceptually, critical sections are a deadlock avoidance layer on top of +// per-object locks. These helpers, in combination with those locks, replace +// our usage of the global interpreter lock to provide thread-safety for +// otherwise thread-unsafe objects, such as dict. +// +// NOTE: These APIs are no-ops in non-free-threaded builds. +// +// NOTE: Only the top-most critical section is guaranteed to be active. +// Operations that need to lock two objects at once must use +// `Py_BEGIN_CRITICAL_SECTION2()`. You *CANNOT* use nested critical sections +// to lock more than one object at once, because the inner critical section +// may suspend the outer critical sections. This API does not provide a way +// to lock more than two objects at once (though it could be added later +// if actually needed). +// +// NOTE: Critical sections implicitly behave like reentrant locks because +// attempting to acquire the same lock will suspend any outer (earlier) +// critical sections. However, they are less efficient for this use case than +// purposefully designed reentrant locks. +// +// Example usage: +// Py_BEGIN_CRITICAL_SECTION(op); +// ... +// Py_END_CRITICAL_SECTION(); +// +// To lock two objects at once: +// Py_BEGIN_CRITICAL_SECTION2(op1, op2); +// ... +// Py_END_CRITICAL_SECTION2(); + +// NOTE: the contents of this struct are private and their meaning may +// change betweeen Python releases without a deprecation period. +typedef struct PyCriticalSection { + // Tagged pointer to an outer active critical section (or 0). + uintptr_t _cs_prev; + + // Mutex used to protect critical section + struct PyMutex *_cs_mutex; +} PyCriticalSection; + +// A critical section protected by two mutexes. Use +// Py_BEGIN_CRITICAL_SECTION2 and Py_END_CRITICAL_SECTION2. +// NOTE: the contents of this struct are private and may change betweeen +// Python releases without a deprecation period. +typedef struct PyCriticalSection2 { + PyCriticalSection _cs_base; + + struct PyMutex *_cs_mutex2; +} PyCriticalSection2; + +PyAPI_FUNC(void) +PyCriticalSection_Begin(PyCriticalSection *c, PyObject *op); + +PyAPI_FUNC(void) +PyCriticalSection_End(PyCriticalSection *c); + +PyAPI_FUNC(void) +PyCriticalSection2_Begin(PyCriticalSection2 *c, PyObject *a, PyObject *b); + +PyAPI_FUNC(void) +PyCriticalSection2_End(PyCriticalSection2 *c); + +// These are definitions for the stable ABI. For GIL-ful builds they're +// conditionally redefined as no-ops in cpython/critical_section.h. + +# define Py_BEGIN_CRITICAL_SECTION(op) \ + { \ + PyCriticalSection _py_cs; \ + PyCriticalSection_Begin(&_py_cs, _PyObject_CAST(op)) + +# define Py_END_CRITICAL_SECTION() \ + PyCriticalSection_End(&_py_cs); \ + } + +# define Py_BEGIN_CRITICAL_SECTION2(a, b) \ + { \ + PyCriticalSection2 _py_cs2; \ + PyCriticalSection2_Begin(&_py_cs2, _PyObject_CAST(a), _PyObject_CAST(b)) + +# define Py_END_CRITICAL_SECTION2() \ + PyCriticalSection2_End(&_py_cs2); \ + } + #ifndef Py_LIMITED_API # define Py_CPYTHON_CRITICAL_SECTION_H # include "cpython/critical_section.h" diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c index a880cb82811f78..46d00f3845eaa6 100644 --- a/Lib/test/test_cext/extension.c +++ b/Lib/test/test_cext/extension.c @@ -99,6 +99,10 @@ _testcext_exec(PyObject *module) obj = NULL; Py_CLEAR(obj); + // Test that Py_BEGIN_CRITICAL_SECTION is available + Py_BEGIN_CRITICAL_SECTION(module); + Py_END_CRITICAL_SECTION(); + return 0; } diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index ed0868e0017fce..059318af64c7a6 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -134,6 +134,10 @@ def test_windows_feature_macros(self): "PyComplex_ImagAsDouble", "PyComplex_RealAsDouble", "PyComplex_Type", + "PyCriticalSection2_Begin", + "PyCriticalSection2_End", + "PyCriticalSection_Begin", + "PyCriticalSection_End", "PyDescr_NewClassMethod", "PyDescr_NewGetSet", "PyDescr_NewMember", diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml index 101737a27829c9..1038765fe27562 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -2689,6 +2689,28 @@ added = '3.15' [function.PyType_GetModuleByToken_DuringGC] added = '3.15' +[function.PyCriticalSection_Begin] + added = '3.15' +[function.PyCriticalSection_End] + added = '3.15' +[struct.PyCriticalSection] + added = '3.15' + struct_abi_kind = 'full-abi' +[macro.Py_BEGIN_CRITICAL_SECTION] + added = '3.15' +[macro.Py_END_CRITICAL_SECTION] + added = '3.15' +[function.PyCriticalSection2_Begin] + added = '3.15' +[function.PyCriticalSection2_End] + added = '3.15' +[struct.PyCriticalSection2] + added = '3.15' + struct_abi_kind = 'full-abi' +[macro.Py_BEGIN_CRITICAL_SECTION2] + added = '3.15' +[macro.Py_END_CRITICAL_SECTION2] + added = '3.15' # PEP 757 import/export API. diff --git a/PC/python3dll.c b/PC/python3dll.c index abbe35c342c13e..b5f9cb15264617 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -174,6 +174,10 @@ EXPORT_FUNC(PyCodec_XMLCharRefReplaceErrors) EXPORT_FUNC(PyComplex_FromDoubles) EXPORT_FUNC(PyComplex_ImagAsDouble) EXPORT_FUNC(PyComplex_RealAsDouble) +EXPORT_FUNC(PyCriticalSection2_Begin) +EXPORT_FUNC(PyCriticalSection2_End) +EXPORT_FUNC(PyCriticalSection_Begin) +EXPORT_FUNC(PyCriticalSection_End) EXPORT_FUNC(PyDescr_NewClassMethod) EXPORT_FUNC(PyDescr_NewGetSet) EXPORT_FUNC(PyDescr_NewMember) From d93282d331f0484ef34911b09e0993b80ce8558d Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 1 May 2026 14:50:47 +0200 Subject: [PATCH 2/3] Add blurb --- .../next/C_API/2026-05-01-14-49-09.gh-issue-149225.IdAYPZ.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/C_API/2026-05-01-14-49-09.gh-issue-149225.IdAYPZ.rst diff --git a/Misc/NEWS.d/next/C_API/2026-05-01-14-49-09.gh-issue-149225.IdAYPZ.rst b/Misc/NEWS.d/next/C_API/2026-05-01-14-49-09.gh-issue-149225.IdAYPZ.rst new file mode 100644 index 00000000000000..98d716ab5fa764 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-05-01-14-49-09.gh-issue-149225.IdAYPZ.rst @@ -0,0 +1,2 @@ +:c:type:`PyCriticalSection` and related functions are added to the Stable +ABI. From 7fe42f43b09e0539c5028d88711e84e948d35b2a Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 1 May 2026 15:49:42 +0200 Subject: [PATCH 3/3] Clarify the functions are no-ops in GIL builds --- Doc/c-api/synchronization.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/c-api/synchronization.rst b/Doc/c-api/synchronization.rst index 4df68564941e4b..7e9894f4d692d6 100644 --- a/Doc/c-api/synchronization.rst +++ b/Doc/c-api/synchronization.rst @@ -220,6 +220,9 @@ are not available. To be used only as in the macro expansions listed :ref:`earlier in this section `. + In non-:term:`free-threaded ` builds of CPython, these + functions do nothing. + .. versionadded:: 3.13 .. c:type:: PyCriticalSection @@ -240,6 +243,9 @@ are not available. To be used only as in the macro expansions listed :ref:`earlier in this section `. + In non-:term:`free-threaded ` builds of CPython, these + functions do nothing. + .. versionadded:: 3.14