Skip to content

Commit 4dc27bc

Browse files
authored
[3.13] gh-119344: Make critical section API public (GH-119353) (#120856)
This makes the following macros public as part of the non-limited C-API for locking a single object or two objects at once. * `Py_BEGIN_CRITICAL_SECTION(op)` / `Py_END_CRITICAL_SECTION()` * `Py_BEGIN_CRITICAL_SECTION2(a, b)` / `Py_END_CRITICAL_SECTION2()` The supporting functions and structs used by the macros are also exposed for cases where C macros are not available. (cherry picked from commit 8f17d69)
1 parent e748805 commit 4dc27bc

15 files changed

+457
-223
lines changed

Doc/c-api/init.rst

+104
Original file line numberDiff line numberDiff line change
@@ -2202,3 +2202,107 @@ The C-API provides a basic mutual exclusion lock.
22022202
issue a fatal error.
22032203
22042204
.. versionadded:: 3.13
2205+
2206+
.. _python-critical-section-api:
2207+
2208+
Python Critical Section API
2209+
---------------------------
2210+
2211+
The critical section API provides a deadlock avoidance layer on top of
2212+
per-object locks for :term:`free-threaded <free threading>` CPython. They are
2213+
intended to replace reliance on the :term:`global interpreter lock`, and are
2214+
no-ops in versions of Python with the global interpreter lock.
2215+
2216+
Critical sections avoid deadlocks by implicitly suspending active critical
2217+
sections and releasing the locks during calls to :c:func:`PyEval_SaveThread`.
2218+
When :c:func:`PyEval_RestoreThread` is called, the most recent critical section
2219+
is resumed, and its locks reacquired. This means the critical section API
2220+
provides weaker guarantees than traditional locks -- they are useful because
2221+
their behavior is similar to the :term:`GIL`.
2222+
2223+
The functions and structs used by the macros are exposed for cases
2224+
where C macros are not available. They should only be used as in the
2225+
given macro expansions. Note that the sizes and contents of the structures may
2226+
change in future Python versions.
2227+
2228+
.. note::
2229+
2230+
Operations that need to lock two objects at once must use
2231+
:c:macro:`Py_BEGIN_CRITICAL_SECTION2`. You *cannot* use nested critical
2232+
sections to lock more than one object at once, because the inner critical
2233+
section may suspend the outer critical sections. This API does not provide
2234+
a way to lock more than two objects at once.
2235+
2236+
Example usage::
2237+
2238+
static PyObject *
2239+
set_field(MyObject *self, PyObject *value)
2240+
{
2241+
Py_BEGIN_CRITICAL_SECTION(self);
2242+
Py_SETREF(self->field, Py_XNewRef(value));
2243+
Py_END_CRITICAL_SECTION();
2244+
Py_RETURN_NONE;
2245+
}
2246+
2247+
In the above example, :c:macro:`Py_SETREF` calls :c:macro:`Py_DECREF`, which
2248+
can call arbitrary code through an object's deallocation function. The critical
2249+
section API avoids potentital deadlocks due to reentrancy and lock ordering
2250+
by allowing the runtime to temporarily suspend the critical section if the
2251+
code triggered by the finalizer blocks and calls :c:func:`PyEval_SaveThread`.
2252+
2253+
.. c:macro:: Py_BEGIN_CRITICAL_SECTION(op)
2254+
2255+
Acquires the per-object lock for the object *op* and begins a
2256+
critical section.
2257+
2258+
In the free-threaded build, this macro expands to::
2259+
2260+
{
2261+
PyCriticalSection _py_cs;
2262+
PyCriticalSection_Begin(&_py_cs, (PyObject*)(op))
2263+
2264+
In the default build, this macro expands to ``{``.
2265+
2266+
.. versionadded:: 3.13
2267+
2268+
.. c:macro:: Py_END_CRITICAL_SECTION()
2269+
2270+
Ends the critical section and releases the per-object lock.
2271+
2272+
In the free-threaded build, this macro expands to::
2273+
2274+
PyCriticalSection_End(&_py_cs);
2275+
}
2276+
2277+
In the default build, this macro expands to ``}``.
2278+
2279+
.. versionadded:: 3.13
2280+
2281+
.. c:macro:: Py_BEGIN_CRITICAL_SECTION2(a, b)
2282+
2283+
Acquires the per-objects locks for the objects *a* and *b* and begins a
2284+
critical section. The locks are acquired in a consistent order (lowest
2285+
address first) to avoid lock ordering deadlocks.
2286+
2287+
In the free-threaded build, this macro expands to::
2288+
2289+
{
2290+
PyCriticalSection2 _py_cs2;
2291+
PyCriticalSection_Begin2(&_py_cs2, (PyObject*)(a), (PyObject*)(b))
2292+
2293+
In the default build, this macro expands to ``{``.
2294+
2295+
.. versionadded:: 3.13
2296+
2297+
.. c:macro:: Py_END_CRITICAL_SECTION2()
2298+
2299+
Ends the critical section and releases the per-object locks.
2300+
2301+
In the free-threaded build, this macro expands to::
2302+
2303+
PyCriticalSection_End2(&_py_cs2);
2304+
}
2305+
2306+
In the default build, this macro expands to ``}``.
2307+
2308+
.. versionadded:: 3.13

Include/Python.h

+1
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
#include "import.h"
125125
#include "abstract.h"
126126
#include "bltinmodule.h"
127+
#include "critical_section.h"
127128
#include "cpython/pyctype.h"
128129
#include "pystrtod.h"
129130
#include "pystrcmp.h"

Include/cpython/critical_section.h

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#ifndef Py_CPYTHON_CRITICAL_SECTION_H
2+
# error "this header file must not be included directly"
3+
#endif
4+
5+
// Python critical sections
6+
//
7+
// Conceptually, critical sections are a deadlock avoidance layer on top of
8+
// per-object locks. These helpers, in combination with those locks, replace
9+
// our usage of the global interpreter lock to provide thread-safety for
10+
// otherwise thread-unsafe objects, such as dict.
11+
//
12+
// NOTE: These APIs are no-ops in non-free-threaded builds.
13+
//
14+
// Straightforward per-object locking could introduce deadlocks that were not
15+
// present when running with the GIL. Threads may hold locks for multiple
16+
// objects simultaneously because Python operations can nest. If threads were
17+
// to acquire the same locks in different orders, they would deadlock.
18+
//
19+
// One way to avoid deadlocks is to allow threads to hold only the lock (or
20+
// locks) for a single operation at a time (typically a single lock, but some
21+
// operations involve two locks). When a thread begins a nested operation it
22+
// could suspend the locks for any outer operation: before beginning the nested
23+
// operation, the locks for the outer operation are released and when the
24+
// nested operation completes, the locks for the outer operation are
25+
// reacquired.
26+
//
27+
// To improve performance, this API uses a variation of the above scheme.
28+
// Instead of immediately suspending locks any time a nested operation begins,
29+
// locks are only suspended if the thread would block. This reduces the number
30+
// of lock acquisitions and releases for nested operations, while still
31+
// avoiding deadlocks.
32+
//
33+
// Additionally, the locks for any active operation are suspended around
34+
// other potentially blocking operations, such as I/O. This is because the
35+
// interaction between locks and blocking operations can lead to deadlocks in
36+
// the same way as the interaction between multiple locks.
37+
//
38+
// Each thread's critical sections and their corresponding locks are tracked in
39+
// a stack in `PyThreadState.critical_section`. When a thread calls
40+
// `_PyThreadState_Detach()`, such as before a blocking I/O operation or when
41+
// waiting to acquire a lock, the thread suspends all of its active critical
42+
// sections, temporarily releasing the associated locks. When the thread calls
43+
// `_PyThreadState_Attach()`, it resumes the top-most (i.e., most recent)
44+
// critical section by reacquiring the associated lock or locks. See
45+
// `_PyCriticalSection_Resume()`.
46+
//
47+
// NOTE: Only the top-most critical section is guaranteed to be active.
48+
// Operations that need to lock two objects at once must use
49+
// `Py_BEGIN_CRITICAL_SECTION2()`. You *CANNOT* use nested critical sections
50+
// to lock more than one object at once, because the inner critical section
51+
// may suspend the outer critical sections. This API does not provide a way
52+
// to lock more than two objects at once (though it could be added later
53+
// if actually needed).
54+
//
55+
// NOTE: Critical sections implicitly behave like reentrant locks because
56+
// attempting to acquire the same lock will suspend any outer (earlier)
57+
// critical sections. However, they are less efficient for this use case than
58+
// purposefully designed reentrant locks.
59+
//
60+
// Example usage:
61+
// Py_BEGIN_CRITICAL_SECTION(op);
62+
// ...
63+
// Py_END_CRITICAL_SECTION();
64+
//
65+
// To lock two objects at once:
66+
// Py_BEGIN_CRITICAL_SECTION2(op1, op2);
67+
// ...
68+
// Py_END_CRITICAL_SECTION2();
69+
70+
typedef struct PyCriticalSection PyCriticalSection;
71+
typedef struct PyCriticalSection2 PyCriticalSection2;
72+
73+
PyAPI_FUNC(void)
74+
PyCriticalSection_Begin(PyCriticalSection *c, PyObject *op);
75+
76+
PyAPI_FUNC(void)
77+
PyCriticalSection_End(PyCriticalSection *c);
78+
79+
PyAPI_FUNC(void)
80+
PyCriticalSection2_Begin(PyCriticalSection2 *c, PyObject *a, PyObject *b);
81+
82+
PyAPI_FUNC(void)
83+
PyCriticalSection2_End(PyCriticalSection2 *c);
84+
85+
#ifndef Py_GIL_DISABLED
86+
# define Py_BEGIN_CRITICAL_SECTION(op) \
87+
{
88+
# define Py_END_CRITICAL_SECTION() \
89+
}
90+
# define Py_BEGIN_CRITICAL_SECTION2(a, b) \
91+
{
92+
# define Py_END_CRITICAL_SECTION2() \
93+
}
94+
#else /* !Py_GIL_DISABLED */
95+
96+
// NOTE: the contents of this struct are private and may change betweeen
97+
// Python releases without a deprecation period.
98+
struct PyCriticalSection {
99+
// Tagged pointer to an outer active critical section (or 0).
100+
uintptr_t _cs_prev;
101+
102+
// Mutex used to protect critical section
103+
PyMutex *_cs_mutex;
104+
};
105+
106+
// A critical section protected by two mutexes. Use
107+
// Py_BEGIN_CRITICAL_SECTION2 and Py_END_CRITICAL_SECTION2.
108+
// NOTE: the contents of this struct are private and may change betweeen
109+
// Python releases without a deprecation period.
110+
struct PyCriticalSection2 {
111+
PyCriticalSection _cs_base;
112+
113+
PyMutex *_cs_mutex2;
114+
};
115+
116+
# define Py_BEGIN_CRITICAL_SECTION(op) \
117+
{ \
118+
PyCriticalSection _py_cs; \
119+
PyCriticalSection_Begin(&_py_cs, _PyObject_CAST(op))
120+
121+
# define Py_END_CRITICAL_SECTION() \
122+
PyCriticalSection_End(&_py_cs); \
123+
}
124+
125+
# define Py_BEGIN_CRITICAL_SECTION2(a, b) \
126+
{ \
127+
PyCriticalSection2 _py_cs2; \
128+
PyCriticalSection2_Begin(&_py_cs2, _PyObject_CAST(a), _PyObject_CAST(b))
129+
130+
# define Py_END_CRITICAL_SECTION2() \
131+
PyCriticalSection2_End(&_py_cs2); \
132+
}
133+
134+
#endif

Include/critical_section.h

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#ifndef Py_CRITICAL_SECTION_H
2+
#define Py_CRITICAL_SECTION_H
3+
#ifdef __cplusplus
4+
extern "C" {
5+
#endif
6+
7+
#ifndef Py_LIMITED_API
8+
# define Py_CPYTHON_CRITICAL_SECTION_H
9+
# include "cpython/critical_section.h"
10+
# undef Py_CPYTHON_CRITICAL_SECTION_H
11+
#endif
12+
13+
#ifdef __cplusplus
14+
}
15+
#endif
16+
#endif /* !Py_CRITICAL_SECTION_H */

0 commit comments

Comments
 (0)