Skip to content

Commit be1dfcc

Browse files
authored
gh-118727: Don't drop the GIL in drop_gil() unless the current thread holds it (#118745)
`drop_gil()` assumes that its caller is attached, which means that the current thread holds the GIL if and only if the GIL is enabled, and the enabled-state of the GIL won't change. This isn't true, though, because `detach_thread()` calls `_PyEval_ReleaseLock()` after detaching and `_PyThreadState_DeleteCurrent()` calls it after removing the current thread from consideration for stop-the-world requests (effectively detaching it). Fix this by remembering whether or not a thread acquired the GIL when it last attached, in `PyThreadState._status.holds_gil`, and check this in `drop_gil()` instead of `gil->enabled`. This fixes a crash in `test_multiprocessing_pool_circular_import()`, so I've reenabled it.
1 parent b30d30c commit be1dfcc

File tree

5 files changed

+68
-55
lines changed

5 files changed

+68
-55
lines changed

Include/cpython/pystate.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,16 @@ struct _ts {
8383
unsigned int bound_gilstate:1;
8484
/* Currently in use (maybe holds the GIL). */
8585
unsigned int active:1;
86+
/* Currently holds the GIL. */
87+
unsigned int holds_gil:1;
8688

8789
/* various stages of finalization */
8890
unsigned int finalizing:1;
8991
unsigned int cleared:1;
9092
unsigned int finalized:1;
9193

9294
/* padding to align to 4 bytes */
93-
unsigned int :24;
95+
unsigned int :23;
9496
} _status;
9597
#ifdef Py_BUILD_CORE
9698
# define _PyThreadState_WHENCE_NOTSET -1

Include/internal/pycore_ceval.h

+3-4
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,10 @@ extern int _PyEval_ThreadsInitialized(void);
131131
extern void _PyEval_InitGIL(PyThreadState *tstate, int own_gil);
132132
extern void _PyEval_FiniGIL(PyInterpreterState *interp);
133133

134-
// Acquire the GIL and return 1. In free-threaded builds, this function may
135-
// return 0 to indicate that the GIL was disabled and therefore not acquired.
136-
extern int _PyEval_AcquireLock(PyThreadState *tstate);
134+
extern void _PyEval_AcquireLock(PyThreadState *tstate);
137135

138-
extern void _PyEval_ReleaseLock(PyInterpreterState *, PyThreadState *);
136+
extern void _PyEval_ReleaseLock(PyInterpreterState *, PyThreadState *,
137+
int final_release);
139138

140139
#ifdef Py_GIL_DISABLED
141140
// Returns 0 or 1 if the GIL for the given thread's interpreter is disabled or

Lib/test/test_importlib/test_threaded_import.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from test.support import verbose
1818
from test.support.import_helper import forget, mock_register_at_fork
1919
from test.support.os_helper import (TESTFN, unlink, rmtree)
20-
from test.support import script_helper, threading_helper, requires_gil_enabled
20+
from test.support import script_helper, threading_helper
2121

2222
threading_helper.requires_working_threading(module=True)
2323

@@ -248,9 +248,6 @@ def test_concurrent_futures_circular_import(self):
248248
'partial', 'cfimport.py')
249249
script_helper.assert_python_ok(fn)
250250

251-
# gh-118727 and gh-118729: pool_in_threads.py may crash in free-threaded
252-
# builds, which can hang the Tsan test so temporarily skip it for now.
253-
@requires_gil_enabled("gh-118727: test may crash in free-threaded builds")
254251
def test_multiprocessing_pool_circular_import(self):
255252
# Regression test for bpo-41567
256253
fn = os.path.join(os.path.dirname(__file__),

Python/ceval_gil.c

+57-39
Original file line numberDiff line numberDiff line change
@@ -205,59 +205,63 @@ static void recreate_gil(struct _gil_runtime_state *gil)
205205
}
206206
#endif
207207

208-
static void
209-
drop_gil_impl(struct _gil_runtime_state *gil)
208+
static inline void
209+
drop_gil_impl(PyThreadState *tstate, struct _gil_runtime_state *gil)
210210
{
211211
MUTEX_LOCK(gil->mutex);
212212
_Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1);
213213
_Py_atomic_store_int_relaxed(&gil->locked, 0);
214+
if (tstate != NULL) {
215+
tstate->_status.holds_gil = 0;
216+
}
214217
COND_SIGNAL(gil->cond);
215218
MUTEX_UNLOCK(gil->mutex);
216219
}
217220

218221
static void
219-
drop_gil(PyInterpreterState *interp, PyThreadState *tstate)
222+
drop_gil(PyInterpreterState *interp, PyThreadState *tstate, int final_release)
220223
{
221224
struct _ceval_state *ceval = &interp->ceval;
222-
/* If tstate is NULL, the caller is indicating that we're releasing
225+
/* If final_release is true, the caller is indicating that we're releasing
223226
the GIL for the last time in this thread. This is particularly
224227
relevant when the current thread state is finalizing or its
225228
interpreter is finalizing (either may be in an inconsistent
226229
state). In that case the current thread will definitely
227230
never try to acquire the GIL again. */
228231
// XXX It may be more correct to check tstate->_status.finalizing.
229-
// XXX assert(tstate == NULL || !tstate->_status.cleared);
232+
// XXX assert(final_release || !tstate->_status.cleared);
230233

234+
assert(final_release || tstate != NULL);
231235
struct _gil_runtime_state *gil = ceval->gil;
232236
#ifdef Py_GIL_DISABLED
233-
if (!_Py_atomic_load_int_relaxed(&gil->enabled)) {
237+
// Check if we have the GIL before dropping it. tstate will be NULL if
238+
// take_gil() detected that this thread has been destroyed, in which case
239+
// we know we have the GIL.
240+
if (tstate != NULL && !tstate->_status.holds_gil) {
234241
return;
235242
}
236243
#endif
237244
if (!_Py_atomic_load_int_relaxed(&gil->locked)) {
238245
Py_FatalError("drop_gil: GIL is not locked");
239246
}
240247

241-
/* tstate is allowed to be NULL (early interpreter init) */
242-
if (tstate != NULL) {
248+
if (!final_release) {
243249
/* Sub-interpreter support: threads might have been switched
244250
under our feet using PyThreadState_Swap(). Fix the GIL last
245251
holder variable so that our heuristics work. */
246252
_Py_atomic_store_ptr_relaxed(&gil->last_holder, tstate);
247253
}
248254

249-
drop_gil_impl(gil);
255+
drop_gil_impl(tstate, gil);
250256

251257
#ifdef FORCE_SWITCHING
252-
/* We check tstate first in case we might be releasing the GIL for
253-
the last time in this thread. In that case there's a possible
254-
race with tstate->interp getting deleted after gil->mutex is
255-
unlocked and before the following code runs, leading to a crash.
256-
We can use (tstate == NULL) to indicate the thread is done with
257-
the GIL, and that's the only time we might delete the
258-
interpreter, so checking tstate first prevents the crash.
259-
See https://github.com/python/cpython/issues/104341. */
260-
if (tstate != NULL &&
258+
/* We might be releasing the GIL for the last time in this thread. In that
259+
case there's a possible race with tstate->interp getting deleted after
260+
gil->mutex is unlocked and before the following code runs, leading to a
261+
crash. We can use final_release to indicate the thread is done with the
262+
GIL, and that's the only time we might delete the interpreter. See
263+
https://github.com/python/cpython/issues/104341. */
264+
if (!final_release &&
261265
_Py_eval_breaker_bit_is_set(tstate, _PY_GIL_DROP_REQUEST_BIT)) {
262266
MUTEX_LOCK(gil->switch_mutex);
263267
/* Not switched yet => wait */
@@ -284,7 +288,7 @@ drop_gil(PyInterpreterState *interp, PyThreadState *tstate)
284288
tstate must be non-NULL.
285289
286290
Returns 1 if the GIL was acquired, or 0 if not. */
287-
static int
291+
static void
288292
take_gil(PyThreadState *tstate)
289293
{
290294
int err = errno;
@@ -309,7 +313,7 @@ take_gil(PyThreadState *tstate)
309313
struct _gil_runtime_state *gil = interp->ceval.gil;
310314
#ifdef Py_GIL_DISABLED
311315
if (!_Py_atomic_load_int_relaxed(&gil->enabled)) {
312-
return 0;
316+
return;
313317
}
314318
#endif
315319

@@ -358,10 +362,10 @@ take_gil(PyThreadState *tstate)
358362
if (!_Py_atomic_load_int_relaxed(&gil->enabled)) {
359363
// Another thread disabled the GIL between our check above and
360364
// now. Don't take the GIL, signal any other waiting threads, and
361-
// return 0.
365+
// return.
362366
COND_SIGNAL(gil->cond);
363367
MUTEX_UNLOCK(gil->mutex);
364-
return 0;
368+
return;
365369
}
366370
#endif
367371

@@ -393,20 +397,21 @@ take_gil(PyThreadState *tstate)
393397
in take_gil() while the main thread called
394398
wait_for_thread_shutdown() from Py_Finalize(). */
395399
MUTEX_UNLOCK(gil->mutex);
396-
/* Passing NULL to drop_gil() indicates that this thread is about to
397-
terminate and will never hold the GIL again. */
398-
drop_gil(interp, NULL);
400+
/* tstate could be a dangling pointer, so don't pass it to
401+
drop_gil(). */
402+
drop_gil(interp, NULL, 1);
399403
PyThread_exit_thread();
400404
}
401405
assert(_PyThreadState_CheckConsistency(tstate));
402406

407+
tstate->_status.holds_gil = 1;
403408
_Py_unset_eval_breaker_bit(tstate, _PY_GIL_DROP_REQUEST_BIT);
404409
update_eval_breaker_for_thread(interp, tstate);
405410

406411
MUTEX_UNLOCK(gil->mutex);
407412

408413
errno = err;
409-
return 1;
414+
return;
410415
}
411416

412417
void _PyEval_SetSwitchInterval(unsigned long microseconds)
@@ -451,10 +456,17 @@ PyEval_ThreadsInitialized(void)
451456
static inline int
452457
current_thread_holds_gil(struct _gil_runtime_state *gil, PyThreadState *tstate)
453458
{
454-
if (((PyThreadState*)_Py_atomic_load_ptr_relaxed(&gil->last_holder)) != tstate) {
455-
return 0;
456-
}
457-
return _Py_atomic_load_int_relaxed(&gil->locked);
459+
int holds_gil = tstate->_status.holds_gil;
460+
461+
// holds_gil is the source of truth; check that last_holder and gil->locked
462+
// are consistent with it.
463+
int locked = _Py_atomic_load_int_relaxed(&gil->locked);
464+
int is_last_holder =
465+
((PyThreadState*)_Py_atomic_load_ptr_relaxed(&gil->last_holder)) == tstate;
466+
assert(!holds_gil || locked);
467+
assert(!holds_gil || is_last_holder);
468+
469+
return holds_gil;
458470
}
459471
#endif
460472

@@ -563,23 +575,24 @@ PyEval_ReleaseLock(void)
563575
/* This function must succeed when the current thread state is NULL.
564576
We therefore avoid PyThreadState_Get() which dumps a fatal error
565577
in debug mode. */
566-
drop_gil(tstate->interp, tstate);
578+
drop_gil(tstate->interp, tstate, 0);
567579
}
568580

569-
int
581+
void
570582
_PyEval_AcquireLock(PyThreadState *tstate)
571583
{
572584
_Py_EnsureTstateNotNULL(tstate);
573-
return take_gil(tstate);
585+
take_gil(tstate);
574586
}
575587

576588
void
577-
_PyEval_ReleaseLock(PyInterpreterState *interp, PyThreadState *tstate)
589+
_PyEval_ReleaseLock(PyInterpreterState *interp,
590+
PyThreadState *tstate,
591+
int final_release)
578592
{
579-
/* If tstate is NULL then we do not expect the current thread
580-
to acquire the GIL ever again. */
581-
assert(tstate == NULL || tstate->interp == interp);
582-
drop_gil(interp, tstate);
593+
assert(tstate != NULL);
594+
assert(tstate->interp == interp);
595+
drop_gil(interp, tstate, final_release);
583596
}
584597

585598
void
@@ -1136,7 +1149,12 @@ _PyEval_DisableGIL(PyThreadState *tstate)
11361149
//
11371150
// Drop the GIL, which will wake up any threads waiting in take_gil()
11381151
// and let them resume execution without the GIL.
1139-
drop_gil_impl(gil);
1152+
drop_gil_impl(tstate, gil);
1153+
1154+
// If another thread asked us to drop the GIL, they should be
1155+
// free-threading by now. Remove any such request so we have a clean
1156+
// slate if/when the GIL is enabled again.
1157+
_Py_unset_eval_breaker_bit(tstate, _PY_GIL_DROP_REQUEST_BIT);
11401158
return 1;
11411159
}
11421160
return 0;

Python/pystate.c

+4-7
Original file line numberDiff line numberDiff line change
@@ -1843,7 +1843,7 @@ _PyThreadState_DeleteCurrent(PyThreadState *tstate)
18431843
#endif
18441844
current_fast_clear(tstate->interp->runtime);
18451845
tstate_delete_common(tstate);
1846-
_PyEval_ReleaseLock(tstate->interp, NULL);
1846+
_PyEval_ReleaseLock(tstate->interp, tstate, 1);
18471847
free_threadstate((_PyThreadStateImpl *)tstate);
18481848
}
18491849

@@ -2068,7 +2068,7 @@ _PyThreadState_Attach(PyThreadState *tstate)
20682068

20692069

20702070
while (1) {
2071-
int acquired_gil = _PyEval_AcquireLock(tstate);
2071+
_PyEval_AcquireLock(tstate);
20722072

20732073
// XXX assert(tstate_is_alive(tstate));
20742074
current_fast_set(&_PyRuntime, tstate);
@@ -2079,20 +2079,17 @@ _PyThreadState_Attach(PyThreadState *tstate)
20792079
}
20802080

20812081
#ifdef Py_GIL_DISABLED
2082-
if (_PyEval_IsGILEnabled(tstate) != acquired_gil) {
2082+
if (_PyEval_IsGILEnabled(tstate) && !tstate->_status.holds_gil) {
20832083
// The GIL was enabled between our call to _PyEval_AcquireLock()
20842084
// and when we attached (the GIL can't go from enabled to disabled
20852085
// here because only a thread holding the GIL can disable
20862086
// it). Detach and try again.
2087-
assert(!acquired_gil);
20882087
tstate_set_detached(tstate, _Py_THREAD_DETACHED);
20892088
tstate_deactivate(tstate);
20902089
current_fast_clear(&_PyRuntime);
20912090
continue;
20922091
}
20932092
_Py_qsbr_attach(((_PyThreadStateImpl *)tstate)->qsbr);
2094-
#else
2095-
(void)acquired_gil;
20962093
#endif
20972094
break;
20982095
}
@@ -2123,7 +2120,7 @@ detach_thread(PyThreadState *tstate, int detached_state)
21232120
tstate_deactivate(tstate);
21242121
tstate_set_detached(tstate, detached_state);
21252122
current_fast_clear(&_PyRuntime);
2126-
_PyEval_ReleaseLock(tstate->interp, tstate);
2123+
_PyEval_ReleaseLock(tstate->interp, tstate, 0);
21272124
}
21282125

21292126
void

0 commit comments

Comments
 (0)