Skip to content

Commit 6845b13

Browse files
committed
critical_section: helpers for fine-grained locking
Critical sections are helpers to replace the global interpreter lock with finer grained locking. They provide similar guarantees to the GIL and avoid the deadlock risk that plain locking involves. Critical sections are implicitly ended whenever the GIL would be released. They are resumed when the GIL would be acquired. Nested critical sections behave as-if they're interleaved.
1 parent 4584be5 commit 6845b13

File tree

9 files changed

+160
-0
lines changed

9 files changed

+160
-0
lines changed

Include/cpython/pystate.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ struct _ts {
172172
int trash_delete_nesting;
173173
PyObject *trash_delete_later;
174174

175+
uintptr_t critical_section;
176+
175177
/* Called when a thread state is deleted normally, but not when it
176178
* is destroyed after fork().
177179
* Pain: to prevent rare but fatal shutdown errors (issue 18808),

Makefile.pre.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ PYTHON_OBJS= \
379379
Python/codecs.o \
380380
Python/compile.o \
381381
Python/context.o \
382+
Python/critical_section.o \
382383
Python/dynamic_annotations.o \
383384
Python/errors.o \
384385
Python/frame.o \
@@ -1643,6 +1644,7 @@ PYTHON_HEADERS= \
16431644
$(srcdir)/Include/internal/pycore_compile.h \
16441645
$(srcdir)/Include/internal/pycore_condvar.h \
16451646
$(srcdir)/Include/internal/pycore_context.h \
1647+
$(srcdir)/Include/internal/pycore_critical_section.h \
16461648
$(srcdir)/Include/internal/pycore_dict.h \
16471649
$(srcdir)/Include/internal/pycore_dict_state.h \
16481650
$(srcdir)/Include/internal/pycore_descrobject.h \

Modules/_testinternalcapi.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "pycore_atomic_funcs.h" // _Py_atomic_int_get()
1616
#include "pycore_bitutils.h" // _Py_bswap32()
1717
#include "pycore_compile.h" // _PyCompile_CodeGen, _PyCompile_OptimizeCfg
18+
#include "pycore_critical_section.h"
1819
#include "pycore_fileutils.h" // _Py_normpath
1920
#include "pycore_frame.h" // _PyInterpreterFrame
2021
#include "pycore_gc.h" // PyGC_Head
@@ -244,6 +245,34 @@ test_hashtable(PyObject *self, PyObject *Py_UNUSED(args))
244245
Py_RETURN_NONE;
245246
}
246247

248+
static PyObject *
249+
test_critical_sections(PyObject *self, PyObject *Py_UNUSED(args))
250+
{
251+
PyThreadState *tstate = PyThreadState_GET();
252+
_PyMutex m1, m2;
253+
memset(&m1, 0, sizeof(m1));
254+
memset(&m2, 0, sizeof(m2));
255+
256+
struct _Py_critical_section c;
257+
_Py_critical_section_begin(&c, &m1);
258+
assert(_PyMutex_is_locked(&m1));
259+
260+
/* nested critical section re-using lock */
261+
struct _Py_critical_section c2;
262+
_Py_critical_section_begin(&c2, &m1);
263+
assert(_PyMutex_is_locked(&m1));
264+
assert(_Py_critical_section_is_active(tstate->critical_section));
265+
assert(!_Py_critical_section_is_active(c2.prev));
266+
_Py_critical_section_end(&c2);
267+
268+
/* mutex is re-locked */
269+
assert(_PyMutex_is_locked(&m1));
270+
271+
_Py_critical_section_end(&c);
272+
assert(!_PyMutex_is_locked(&m1));
273+
274+
Py_RETURN_NONE;
275+
}
247276

248277
static PyObject *
249278
test_get_config(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args))
@@ -634,6 +663,7 @@ static PyMethodDef TestMethods[] = {
634663
_TESTINTERNALCAPI_COMPILER_CODEGEN_METHODDEF
635664
_TESTINTERNALCAPI_OPTIMIZE_CFG_METHODDEF
636665
{"get_interp_settings", get_interp_settings, METH_VARARGS, NULL},
666+
{"test_critical_sections", test_critical_sections, METH_NOARGS},
637667
{NULL, NULL} /* sentinel */
638668
};
639669

PCbuild/_freeze_module.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
<ClCompile Include="..\Python\codecs.c" />
187187
<ClCompile Include="..\Python\compile.c" />
188188
<ClCompile Include="..\Python\context.c" />
189+
<ClCompile Include="..\Python\critical_section.c" />
189190
<ClCompile Include="..\Python\dtoa.c" />
190191
<ClCompile Include="..\Python\dynamic_annotations.c" />
191192
<ClCompile Include="..\Python\dynload_win.c" />

PCbuild/_freeze_module.vcxproj.filters

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@
100100
<ClCompile Include="..\Python\context.c">
101101
<Filter>Source Files</Filter>
102102
</ClCompile>
103+
<ClCompile Include="..\Python\critical_section.c">
104+
<Filter>Source Files</Filter>
105+
</ClCompile>
103106
<ClCompile Include="..\Objects\descrobject.c">
104107
<Filter>Source Files</Filter>
105108
</ClCompile>

PCbuild/pythoncore.vcxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@
210210
<ClInclude Include="..\Include\internal\pycore_compile.h" />
211211
<ClInclude Include="..\Include\internal\pycore_condvar.h" />
212212
<ClInclude Include="..\Include\internal\pycore_context.h" />
213+
<ClInclude Include="..\Include\internal\pycore_critical_section.h" />
213214
<ClInclude Include="..\Include\internal\pycore_descrobject.h" />
214215
<ClInclude Include="..\Include\internal\pycore_dict.h" />
215216
<ClInclude Include="..\Include\internal\pycore_dict_state.h" />
@@ -508,6 +509,7 @@
508509
<ClCompile Include="..\Python\codecs.c" />
509510
<ClCompile Include="..\Python\compile.c" />
510511
<ClCompile Include="..\Python\context.c" />
512+
<ClCompile Include="..\Python\critical_section.c" />
511513
<ClCompile Include="..\Python\dynamic_annotations.c" />
512514
<ClCompile Include="..\Python\dynload_win.c" />
513515
<ClCompile Include="..\Python\errors.c" />

PCbuild/pythoncore.vcxproj.filters

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,9 @@
537537
<ClInclude Include="..\Include\internal\pycore_context.h">
538538
<Filter>Include\internal</Filter>
539539
</ClInclude>
540+
<ClInclude Include="..\Include\internal\pycore_critical_section.h">
541+
<Filter>Include\internal</Filter>
542+
</ClInclude>
540543
<ClInclude Include="..\Include\internal\pycore_descrobject.h">
541544
<Filter>Include\internal</Filter>
542545
</ClInclude>
@@ -1118,6 +1121,9 @@
11181121
<ClCompile Include="..\Python\compile.c">
11191122
<Filter>Python</Filter>
11201123
</ClCompile>
1124+
<ClCompile Include="..\Python\critical_section.c">
1125+
<Filter>Python</Filter>
1126+
</ClCompile>
11211127
<ClCompile Include="..\Python\dynamic_annotations.c">
11221128
<Filter>Python</Filter>
11231129
</ClCompile>

Python/critical_section.c

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#include "Python.h"
2+
#include "pycore_critical_section.h"
3+
4+
void
5+
_Py_critical_section_begin_slow(struct _Py_critical_section *c, _PyMutex *m)
6+
{
7+
PyThreadState *tstate = PyThreadState_GET();
8+
c->mutex = NULL;
9+
c->prev = (uintptr_t)tstate->critical_section;
10+
tstate->critical_section = (uintptr_t)c;
11+
12+
_PyMutex_lock(m);
13+
c->mutex = m;
14+
}
15+
16+
void
17+
_Py_critical_section2_begin_slow(struct _Py_critical_section2 *c,
18+
_PyMutex *m1, _PyMutex *m2, int flag)
19+
{
20+
PyThreadState *tstate = PyThreadState_GET();
21+
c->base.mutex = NULL;
22+
c->mutex2 = NULL;
23+
c->base.prev = tstate->critical_section;
24+
tstate->critical_section = (uintptr_t)c | _Py_CRITICAL_SECTION_TWO_MUTEXES;
25+
26+
if (!flag) {
27+
_PyMutex_lock(m1);
28+
}
29+
_PyMutex_lock(m2);
30+
c->base.mutex = m1;
31+
c->mutex2 = m2;
32+
}
33+
34+
struct _Py_critical_section *
35+
_Py_critical_section_untag(uintptr_t tag)
36+
{
37+
tag &= ~_Py_CRITICAL_SECTION_MASK;
38+
return (struct _Py_critical_section *)tag;
39+
}
40+
41+
// Release all locks held by critical sections. This is called by
42+
// _PyThreadState_Detach.
43+
void
44+
_Py_critical_section_end_all(PyThreadState *tstate)
45+
{
46+
uintptr_t *tagptr;
47+
struct _Py_critical_section *c;
48+
struct _Py_critical_section2 *c2;
49+
50+
tagptr = &tstate->critical_section;
51+
while (*tagptr && _Py_critical_section_is_active(*tagptr)) {
52+
c = _Py_critical_section_untag(*tagptr);
53+
54+
if (c->mutex) {
55+
_PyMutex_unlock(c->mutex);
56+
if ((*tagptr & _Py_CRITICAL_SECTION_TWO_MUTEXES)) {
57+
c2 = (struct _Py_critical_section2 *)c;
58+
if (c2->mutex2) {
59+
_PyMutex_unlock(c2->mutex2);
60+
}
61+
}
62+
}
63+
64+
*tagptr |= _Py_CRITICAL_SECTION_INACTIVE;
65+
tagptr = &c->prev;
66+
}
67+
}
68+
69+
void
70+
_Py_critical_section_resume(PyThreadState *tstate)
71+
{
72+
uintptr_t p;
73+
struct _Py_critical_section *c;
74+
struct _Py_critical_section2 *c2;
75+
76+
p = tstate->critical_section;
77+
c = _Py_critical_section_untag(p);
78+
assert(!_Py_critical_section_is_active(p));
79+
80+
_PyMutex *m1 = NULL, *m2 = NULL;
81+
82+
m1 = c->mutex;
83+
c->mutex = NULL;
84+
if ((p & _Py_CRITICAL_SECTION_TWO_MUTEXES)) {
85+
c2 = (struct _Py_critical_section2 *)c;
86+
m2 = c2->mutex2;
87+
c2->mutex2 = NULL;
88+
}
89+
90+
if (m1) {
91+
_PyMutex_lock(m1);
92+
}
93+
if (m2) {
94+
_PyMutex_lock(m2);
95+
}
96+
97+
c->mutex = m1;
98+
if (m2) {
99+
c2->mutex2 = m2;
100+
}
101+
102+
tstate->critical_section &= ~_Py_CRITICAL_SECTION_INACTIVE;
103+
}

Python/pystate.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "Python.h"
55
#include "pycore_ceval.h"
66
#include "pycore_code.h" // stats
7+
#include "pycore_critical_section.h"
78
#include "pycore_frame.h"
89
#include "pycore_initconfig.h"
910
#include "pycore_lock.h" // _PyRawEvent
@@ -257,6 +258,12 @@ _PyThreadState_Attach(PyThreadState *tstate)
257258
&tstate->status,
258259
_Py_THREAD_DETACHED,
259260
_Py_THREAD_ATTACHED)) {
261+
262+
// resume previous critical section
263+
if (tstate->critical_section != 0) {
264+
_Py_critical_section_resume(tstate);
265+
}
266+
260267
return 1;
261268
}
262269
return 0;
@@ -265,6 +272,10 @@ _PyThreadState_Attach(PyThreadState *tstate)
265272
static void
266273
_PyThreadState_Detach(PyThreadState *tstate)
267274
{
275+
if (tstate->critical_section != 0) {
276+
_Py_critical_section_end_all(tstate);
277+
}
278+
268279
_Py_atomic_store_int(&tstate->status, _Py_THREAD_DETACHED);
269280
}
270281

0 commit comments

Comments
 (0)