Skip to content

Commit 0fa9fa3

Browse files
committed
gh-117657: Use PyMutex based recursive lock for import lock
This adds a `_PyRecursiveMutex` type based on `PyMutex` and uses that for the import lock. This fixes some data races in the free-threaded build and generally simplifies the import lock code.
1 parent 2770d5c commit 0fa9fa3

File tree

7 files changed

+90
-103
lines changed

7 files changed

+90
-103
lines changed

Include/internal/pycore_import.h

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ PyAPI_FUNC(int) _PyImport_SetModule(PyObject *name, PyObject *module);
2020
extern int _PyImport_SetModuleString(const char *name, PyObject* module);
2121

2222
extern void _PyImport_AcquireLock(PyInterpreterState *interp);
23-
extern int _PyImport_ReleaseLock(PyInterpreterState *interp);
23+
extern void _PyImport_ReleaseLock(PyInterpreterState *interp);
2424

2525
// This is used exclusively for the sys and builtins modules:
2626
extern int _PyImport_FixupBuiltin(
@@ -94,11 +94,7 @@ struct _import_state {
9494
#endif
9595
PyObject *import_func;
9696
/* The global import lock. */
97-
struct {
98-
PyThread_type_lock mutex;
99-
unsigned long thread;
100-
int level;
101-
} lock;
97+
_PyRecursiveMutex lock;
10298
/* diagnostic info in PyImport_ImportModuleLevelObject() */
10399
struct {
104100
int import_level;
@@ -123,11 +119,6 @@ struct _import_state {
123119
#define IMPORTS_INIT \
124120
{ \
125121
DLOPENFLAGS_INIT \
126-
.lock = { \
127-
.mutex = NULL, \
128-
.thread = PYTHREAD_INVALID_THREAD_ID, \
129-
.level = 0, \
130-
}, \
131122
.find_and_load = { \
132123
.header = 1, \
133124
}, \
@@ -180,11 +171,6 @@ extern void _PyImport_FiniCore(PyInterpreterState *interp);
180171
extern void _PyImport_FiniExternal(PyInterpreterState *interp);
181172

182173

183-
#ifdef HAVE_FORK
184-
extern PyStatus _PyImport_ReInitLock(PyInterpreterState *interp);
185-
#endif
186-
187-
188174
extern PyObject* _PyImport_GetBuiltinModuleNames(void);
189175

190176
struct _module_alias {

Include/internal/pycore_lock.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,18 @@ _PyOnceFlag_CallOnce(_PyOnceFlag *flag, _Py_once_fn_t *fn, void *arg)
219219
return _PyOnceFlag_CallOnceSlow(flag, fn, arg);
220220
}
221221

222+
// A recursive mutex. The mutex should zero-initialized.
223+
typedef struct {
224+
PyMutex mutex;
225+
unsigned long long thread; // i.e., PyThread_get_thread_ident_ex()
226+
size_t level;
227+
} _PyRecursiveMutex;
228+
229+
PyAPI_FUNC(int) _PyRecursiveMutex_IsLockedByCurrentThread(_PyRecursiveMutex *m);
230+
PyAPI_FUNC(void) _PyRecursiveMutex_Lock(_PyRecursiveMutex *m);
231+
PyAPI_FUNC(void) _PyRecursiveMutex_Unlock(_PyRecursiveMutex *m);
232+
233+
222234
// A readers-writer (RW) lock. The lock supports multiple concurrent readers or
223235
// a single writer. The lock is write-preferring: if a writer is waiting while
224236
// the lock is read-locked then, new readers will be blocked. This avoids

Modules/_testinternalcapi/test_lock.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "parts.h"
44
#include "pycore_lock.h"
5+
#include "pycore_pythread.h" // PyThread_get_thread_ident_ex()
56
#include "pycore_time.h" // _PyTime_MonotonicUnchecked()
67

78
#include "clinic/test_lock.c.h"
@@ -471,6 +472,29 @@ test_lock_rwlock(PyObject *self, PyObject *obj)
471472
Py_RETURN_NONE;
472473
}
473474

475+
static PyObject *
476+
test_lock_recursive(PyObject *self, PyObject *obj)
477+
{
478+
_PyRecursiveMutex m = (_PyRecursiveMutex){0};
479+
assert(!_PyRecursiveMutex_IsLockedByCurrentThread(&m));
480+
481+
_PyRecursiveMutex_Lock(&m);
482+
assert(m.thread == PyThread_get_thread_ident_ex());
483+
assert(PyMutex_IsLocked(&m.mutex));
484+
assert(m.level == 0);
485+
486+
_PyRecursiveMutex_Lock(&m);
487+
assert(m.level == 1);
488+
_PyRecursiveMutex_Unlock(&m);
489+
490+
_PyRecursiveMutex_Unlock(&m);
491+
assert(m.thread == 0);
492+
assert(!PyMutex_IsLocked(&m.mutex));
493+
assert(m.level == 0);
494+
495+
Py_RETURN_NONE;
496+
}
497+
474498
static PyMethodDef test_methods[] = {
475499
{"test_lock_basic", test_lock_basic, METH_NOARGS},
476500
{"test_lock_two_threads", test_lock_two_threads, METH_NOARGS},
@@ -480,6 +504,7 @@ static PyMethodDef test_methods[] = {
480504
{"test_lock_benchmark", test_lock_benchmark, METH_NOARGS},
481505
{"test_lock_once", test_lock_once, METH_NOARGS},
482506
{"test_lock_rwlock", test_lock_rwlock, METH_NOARGS},
507+
{"test_lock_recursive", test_lock_recursive, METH_NOARGS},
483508
{NULL, NULL} /* sentinel */
484509
};
485510

Modules/posixmodule.c

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
#include "pycore_call.h" // _PyObject_CallNoArgs()
1717
#include "pycore_ceval.h" // _PyEval_ReInitThreads()
1818
#include "pycore_fileutils.h" // _Py_closerange()
19-
#include "pycore_import.h" // _PyImport_ReInitLock()
2019
#include "pycore_initconfig.h" // _PyStatus_EXCEPTION()
2120
#include "pycore_moduleobject.h" // _PyModule_GetState()
2221
#include "pycore_object.h" // _PyObject_LookupSpecial()
@@ -626,10 +625,7 @@ PyOS_AfterFork_Parent(void)
626625
_PyEval_StartTheWorldAll(&_PyRuntime);
627626

628627
PyInterpreterState *interp = _PyInterpreterState_GET();
629-
if (_PyImport_ReleaseLock(interp) <= 0) {
630-
Py_FatalError("failed releasing import lock after fork");
631-
}
632-
628+
_PyImport_ReleaseLock(interp);
633629
run_at_forkers(interp->after_forkers_parent, 0);
634630
}
635631

@@ -674,10 +670,7 @@ PyOS_AfterFork_Child(void)
674670
_PyEval_StartTheWorldAll(&_PyRuntime);
675671
_PyThreadState_DeleteList(list);
676672

677-
status = _PyImport_ReInitLock(tstate->interp);
678-
if (_PyStatus_EXCEPTION(status)) {
679-
goto fatal_error;
680-
}
673+
_PyImport_ReleaseLock(tstate->interp);
681674

682675
_PySignal_AfterFork();
683676

Python/import.c

Lines changed: 7 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,7 @@ static struct _inittab *inittab_copy = NULL;
8282
(interp)->imports.import_func
8383

8484
#define IMPORT_LOCK(interp) \
85-
(interp)->imports.lock.mutex
86-
#define IMPORT_LOCK_THREAD(interp) \
87-
(interp)->imports.lock.thread
88-
#define IMPORT_LOCK_LEVEL(interp) \
89-
(interp)->imports.lock.level
85+
(interp)->imports.lock
9086

9187
#define FIND_AND_LOAD(interp) \
9288
(interp)->imports.find_and_load
@@ -103,74 +99,14 @@ static struct _inittab *inittab_copy = NULL;
10399
void
104100
_PyImport_AcquireLock(PyInterpreterState *interp)
105101
{
106-
unsigned long me = PyThread_get_thread_ident();
107-
if (me == PYTHREAD_INVALID_THREAD_ID)
108-
return; /* Too bad */
109-
if (IMPORT_LOCK(interp) == NULL) {
110-
IMPORT_LOCK(interp) = PyThread_allocate_lock();
111-
if (IMPORT_LOCK(interp) == NULL)
112-
return; /* Nothing much we can do. */
113-
}
114-
if (IMPORT_LOCK_THREAD(interp) == me) {
115-
IMPORT_LOCK_LEVEL(interp)++;
116-
return;
117-
}
118-
if (IMPORT_LOCK_THREAD(interp) != PYTHREAD_INVALID_THREAD_ID ||
119-
!PyThread_acquire_lock(IMPORT_LOCK(interp), 0))
120-
{
121-
PyThreadState *tstate = PyEval_SaveThread();
122-
PyThread_acquire_lock(IMPORT_LOCK(interp), WAIT_LOCK);
123-
PyEval_RestoreThread(tstate);
124-
}
125-
assert(IMPORT_LOCK_LEVEL(interp) == 0);
126-
IMPORT_LOCK_THREAD(interp) = me;
127-
IMPORT_LOCK_LEVEL(interp) = 1;
102+
_PyRecursiveMutex_Lock(&IMPORT_LOCK(interp));
128103
}
129104

130-
int
105+
void
131106
_PyImport_ReleaseLock(PyInterpreterState *interp)
132107
{
133-
unsigned long me = PyThread_get_thread_ident();
134-
if (me == PYTHREAD_INVALID_THREAD_ID || IMPORT_LOCK(interp) == NULL)
135-
return 0; /* Too bad */
136-
if (IMPORT_LOCK_THREAD(interp) != me)
137-
return -1;
138-
IMPORT_LOCK_LEVEL(interp)--;
139-
assert(IMPORT_LOCK_LEVEL(interp) >= 0);
140-
if (IMPORT_LOCK_LEVEL(interp) == 0) {
141-
IMPORT_LOCK_THREAD(interp) = PYTHREAD_INVALID_THREAD_ID;
142-
PyThread_release_lock(IMPORT_LOCK(interp));
143-
}
144-
return 1;
145-
}
146-
147-
#ifdef HAVE_FORK
148-
/* This function is called from PyOS_AfterFork_Child() to ensure that newly
149-
created child processes do not share locks with the parent.
150-
We now acquire the import lock around fork() calls but on some platforms
151-
(Solaris 9 and earlier? see isue7242) that still left us with problems. */
152-
PyStatus
153-
_PyImport_ReInitLock(PyInterpreterState *interp)
154-
{
155-
if (IMPORT_LOCK(interp) != NULL) {
156-
if (_PyThread_at_fork_reinit(&IMPORT_LOCK(interp)) < 0) {
157-
return _PyStatus_ERR("failed to create a new lock");
158-
}
159-
}
160-
161-
if (IMPORT_LOCK_LEVEL(interp) > 1) {
162-
/* Forked as a side effect of import */
163-
unsigned long me = PyThread_get_thread_ident();
164-
PyThread_acquire_lock(IMPORT_LOCK(interp), WAIT_LOCK);
165-
IMPORT_LOCK_THREAD(interp) = me;
166-
IMPORT_LOCK_LEVEL(interp)--;
167-
} else {
168-
IMPORT_LOCK_THREAD(interp) = PYTHREAD_INVALID_THREAD_ID;
169-
IMPORT_LOCK_LEVEL(interp) = 0;
170-
}
171-
return _PyStatus_OK();
108+
_PyRecursiveMutex_Unlock(&IMPORT_LOCK(interp));
172109
}
173-
#endif
174110

175111

176112
/***************/
@@ -3446,11 +3382,6 @@ _PyImport_FiniCore(PyInterpreterState *interp)
34463382
PyErr_FormatUnraisable("Exception ignored on clearing sys.modules");
34473383
}
34483384

3449-
if (IMPORT_LOCK(interp) != NULL) {
3450-
PyThread_free_lock(IMPORT_LOCK(interp));
3451-
IMPORT_LOCK(interp) = NULL;
3452-
}
3453-
34543385
_PyImport_ClearCore(interp);
34553386
}
34563387

@@ -3583,8 +3514,7 @@ _imp_lock_held_impl(PyObject *module)
35833514
/*[clinic end generated code: output=8b89384b5e1963fc input=9b088f9b217d9bdf]*/
35843515
{
35853516
PyInterpreterState *interp = _PyInterpreterState_GET();
3586-
return PyBool_FromLong(
3587-
IMPORT_LOCK_THREAD(interp) != PYTHREAD_INVALID_THREAD_ID);
3517+
return PyBool_FromLong(PyMutex_IsLocked(&IMPORT_LOCK(interp).mutex));
35883518
}
35893519

35903520
/*[clinic input]
@@ -3618,11 +3548,12 @@ _imp_release_lock_impl(PyObject *module)
36183548
/*[clinic end generated code: output=7faab6d0be178b0a input=934fb11516dd778b]*/
36193549
{
36203550
PyInterpreterState *interp = _PyInterpreterState_GET();
3621-
if (_PyImport_ReleaseLock(interp) < 0) {
3551+
if (!_PyRecursiveMutex_IsLockedByCurrentThread(&IMPORT_LOCK(interp))) {
36223552
PyErr_SetString(PyExc_RuntimeError,
36233553
"not holding the import lock");
36243554
return NULL;
36253555
}
3556+
_PyImport_ReleaseLock(interp);
36263557
Py_RETURN_NONE;
36273558
}
36283559

Python/lock.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,48 @@ _PyOnceFlag_CallOnceSlow(_PyOnceFlag *flag, _Py_once_fn_t *fn, void *arg)
362362
}
363363
}
364364

365+
static int
366+
recursive_mutex_is_owned_by(_PyRecursiveMutex *m, PyThread_ident_t tid)
367+
{
368+
return _Py_atomic_load_ullong_relaxed(&m->thread) == tid;
369+
}
370+
371+
int
372+
_PyRecursiveMutex_IsLockedByCurrentThread(_PyRecursiveMutex *m)
373+
{
374+
return recursive_mutex_is_owned_by(m, PyThread_get_thread_ident_ex());
375+
}
376+
377+
void
378+
_PyRecursiveMutex_Lock(_PyRecursiveMutex *m)
379+
{
380+
PyThread_ident_t thread = PyThread_get_thread_ident_ex();
381+
if (recursive_mutex_is_owned_by(m, thread)) {
382+
m->level++;
383+
return;
384+
}
385+
PyMutex_Lock(&m->mutex);
386+
_Py_atomic_store_ullong_relaxed(&m->thread, thread);
387+
assert(m->level == 0);
388+
}
389+
390+
void
391+
_PyRecursiveMutex_Unlock(_PyRecursiveMutex *m)
392+
{
393+
PyThread_ident_t thread = PyThread_get_thread_ident_ex();
394+
if (!recursive_mutex_is_owned_by(m, thread)) {
395+
Py_FatalError("unlocking a recursive mutex that is not owned by the"
396+
" current thread");
397+
}
398+
if (m->level > 0) {
399+
m->level--;
400+
return;
401+
}
402+
assert(m->level == 0);
403+
_Py_atomic_store_ullong_relaxed(&m->thread, 0);
404+
PyMutex_Unlock(&m->mutex);
405+
}
406+
365407
#define _Py_WRITE_LOCKED 1
366408
#define _PyRWMutex_READER_SHIFT 2
367409
#define _Py_RWMUTEX_MAX_READERS (UINTPTR_MAX >> _PyRWMutex_READER_SHIFT)

Tools/tsan/suppressions_free_threading.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ race:_in_weak_set
1616
race:_mi_heap_delayed_free_partial
1717
race:_PyEval_EvalFrameDefault
1818
race:_PyFunction_SetVersion
19-
race:_PyImport_AcquireLock
20-
race:_PyImport_ReleaseLock
2119
race:_PyInterpreterState_SetNotRunningMain
2220
race:_PyInterpreterState_IsRunningMain
2321
race:_PyObject_GC_IS_SHARED

0 commit comments

Comments
 (0)