Skip to content

Commit d2adaf4

Browse files
committed
gh-151518: Track attach waiters in the thread state
Represent active attach waiters with suspended-waiting and detached-waiting states. Preserve waiter registration when the world resumes, and keep passive detached threads immediately parkable. Restore the thread-state padding and retain the single-CAS uncontended attach path.
1 parent 14a870c commit d2adaf4

3 files changed

Lines changed: 47 additions & 52 deletions

File tree

‎Include/internal/pycore_pystate.h‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ extern "C" {
2424
// The "suspended" states are used to implement stop-the-world pauses, such as
2525
// for cyclic garbage collection. They are only used in `--disable-gil` builds.
2626
// They are similar to the "detached" state in that the thread is not allowed
27-
// to call most Python APIs. However, unlike the "detached" state, a thread may
28-
// not transition itself out from a "suspended" state. Only the thread
29-
// performing a stop-the-world pause may transition a thread from a "suspended"
30-
// state back to the "detached" state.
27+
// to call most Python APIs. A suspended thread trying to attach marks itself
28+
// as "suspended-waiting". Only the thread performing a stop-the-world pause
29+
// may resume a suspended thread, moving it to "detached" or "detached-waiting".
30+
// A "detached-waiting" thread must attach before it can be suspended again.
3131
//
3232
// The "shutting down" state is used when the interpreter is being finalized.
3333
// Threads in this state can't do anything other than block the OS thread.
@@ -36,13 +36,16 @@ extern "C" {
3636
// State transitions:
3737
// Bound thread: attached <-> detached
3838
// attached -> suspended
39-
// Stop-the-world thread: detached <-> suspended-detached
40-
// suspended -> detached
39+
// suspended -> suspended-waiting
40+
// detached-waiting -> attached
41+
// Stop-the-world thread: detached <-> suspended
42+
// suspended-waiting -> detached-waiting
4143
#define _Py_THREAD_DETACHED 0
4244
#define _Py_THREAD_ATTACHED 1
4345
#define _Py_THREAD_SUSPENDED 2
4446
#define _Py_THREAD_SHUTTING_DOWN 3
45-
#define _Py_THREAD_SUSPENDED_DETACHED 4
47+
#define _Py_THREAD_SUSPENDED_WAITING 4
48+
#define _Py_THREAD_DETACHED_WAITING 5
4649

4750

4851
/* Check if the current thread is the main thread.

‎Include/internal/pycore_tstate.h‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,8 @@ typedef struct _PyThreadStateImpl {
105105

106106
#ifdef Py_GIL_DISABLED
107107
// gh-144438: Add padding to ensure that the fields above don't share a
108-
// cache line with other allocations. Reuse the first bytes of the padding
109-
// for a cold stop-the-world flag without growing the thread state.
110-
union {
111-
// Set while the thread is waiting to attach after a
112-
// stop-the-world pause suspended it while detached.
113-
int stw_attach_waiting;
114-
char __padding[64];
115-
};
108+
// cache line with other allocations.
109+
char __padding[64];
116110
#endif
117111
} _PyThreadStateImpl;
118112

‎Python/pystate.c‎

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,8 +1939,9 @@ tstate_delete_common(PyThreadState *tstate, int release_gil)
19391939
if (tstate->next) {
19401940
tstate->next->prev = tstate->prev;
19411941
}
1942-
if (tstate->state != _Py_THREAD_SUSPENDED &&
1943-
tstate->state != _Py_THREAD_SUSPENDED_DETACHED)
1942+
int state = _Py_atomic_load_int_relaxed(&tstate->state);
1943+
if (state != _Py_THREAD_SUSPENDED &&
1944+
state != _Py_THREAD_SUSPENDED_WAITING)
19441945
{
19451946
// Any ongoing stop-the-world request should not wait for us because
19461947
// our thread is getting deleted.
@@ -2239,23 +2240,20 @@ tstate_set_detached(PyThreadState *tstate, int detached_state)
22392240
static void
22402241
tstate_wait_attach(PyThreadState *tstate)
22412242
{
2242-
#ifdef Py_GIL_DISABLED
2243-
_PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate;
2244-
int stw_attach_waiting = 0;
2245-
#endif
2246-
do {
2243+
for (;;) {
22472244
int state = _Py_atomic_load_int_relaxed(&tstate->state);
2248-
if (state == _Py_THREAD_SUSPENDED ||
2249-
state == _Py_THREAD_SUSPENDED_DETACHED)
2250-
{
2251-
#ifdef Py_GIL_DISABLED
2252-
if (state == _Py_THREAD_SUSPENDED_DETACHED) {
2253-
stw_attach_waiting = 1;
2254-
_Py_atomic_store_int_relaxed(
2255-
&tstate_impl->stw_attach_waiting, 1);
2245+
if (state == _Py_THREAD_SUSPENDED) {
2246+
// Register an active attach waiter. The next stop-the-world
2247+
// request must let this thread attach before suspending it again.
2248+
if (!_Py_atomic_compare_exchange_int(
2249+
&tstate->state, &state, _Py_THREAD_SUSPENDED_WAITING))
2250+
{
2251+
continue;
22562252
}
2257-
#endif
2258-
// Wait until we're switched out of SUSPENDED to DETACHED.
2253+
state = _Py_THREAD_SUSPENDED_WAITING;
2254+
}
2255+
if (state == _Py_THREAD_SUSPENDED_WAITING) {
2256+
// Wait until the stop-the-world thread lets us attach.
22592257
_PyParkingLot_Park(&tstate->state, &state, sizeof(tstate->state),
22602258
/*timeout=*/-1, NULL, /*detach=*/0);
22612259
}
@@ -2264,15 +2262,15 @@ tstate_wait_attach(PyThreadState *tstate)
22642262
_PyThreadState_HangThread(tstate);
22652263
}
22662264
else {
2267-
assert(state == _Py_THREAD_DETACHED);
2265+
assert(state == _Py_THREAD_DETACHED ||
2266+
state == _Py_THREAD_DETACHED_WAITING);
2267+
if (_Py_atomic_compare_exchange_int(
2268+
&tstate->state, &state, _Py_THREAD_ATTACHED))
2269+
{
2270+
return;
2271+
}
22682272
}
2269-
// Once we're back in DETACHED we can re-attach
2270-
} while (!tstate_try_attach(tstate));
2271-
#ifdef Py_GIL_DISABLED
2272-
if (stw_attach_waiting) {
2273-
_Py_atomic_store_int_relaxed(&tstate_impl->stw_attach_waiting, 0);
22742273
}
2275-
#endif
22762274
}
22772275

22782276
void
@@ -2462,17 +2460,12 @@ park_detached_threads(struct _stoptheworld_state *stw)
24622460
_Py_FOR_EACH_STW_INTERP(stw, i) {
24632461
_Py_FOR_EACH_TSTATE_UNLOCKED(i, t) {
24642462
int state = _Py_atomic_load_int_relaxed(&t->state);
2463+
// DETACHED_WAITING threads remain counted until they attach and
2464+
// stop, so repeated pauses cannot prevent them from attaching.
24652465
if (state == _Py_THREAD_DETACHED) {
2466-
_PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)t;
2467-
if (_Py_atomic_load_int_relaxed(
2468-
&tstate_impl->stw_attach_waiting))
2469-
{
2470-
continue;
2471-
}
24722466
// Atomically transition to "suspended" if in "detached" state.
24732467
if (_Py_atomic_compare_exchange_int(
2474-
&t->state, &state,
2475-
_Py_THREAD_SUSPENDED_DETACHED)) {
2468+
&t->state, &state, _Py_THREAD_SUSPENDED)) {
24762469
num_parked++;
24772470
}
24782471
}
@@ -2557,12 +2550,17 @@ start_the_world(struct _stoptheworld_state *stw)
25572550
_Py_FOR_EACH_STW_INTERP(stw, i) {
25582551
_Py_FOR_EACH_TSTATE_UNLOCKED(i, t) {
25592552
if (t != stw->requester) {
2560-
#ifndef NDEBUG
25612553
int state = _Py_atomic_load_int_relaxed(&t->state);
2562-
assert(state == _Py_THREAD_SUSPENDED ||
2563-
state == _Py_THREAD_SUSPENDED_DETACHED);
2564-
#endif
2565-
_Py_atomic_store_int(&t->state, _Py_THREAD_DETACHED);
2554+
int next_state;
2555+
do {
2556+
assert(state == _Py_THREAD_SUSPENDED ||
2557+
state == _Py_THREAD_SUSPENDED_WAITING);
2558+
next_state = (state == _Py_THREAD_SUSPENDED_WAITING
2559+
? _Py_THREAD_DETACHED_WAITING
2560+
: _Py_THREAD_DETACHED);
2561+
// Retry if an attach waiter registered concurrently.
2562+
} while (!_Py_atomic_compare_exchange_int(
2563+
&t->state, &state, next_state));
25662564
_PyParkingLot_UnparkAll(&t->state);
25672565
}
25682566
}

0 commit comments

Comments
 (0)