Skip to content

Commit bc20719

Browse files
committed
gh-151518: Exercise STW fairness directly
1 parent 3a368a6 commit bc20719

4 files changed

Lines changed: 72 additions & 60 deletions

File tree

‎Lib/test/test_free_threading/test_gc.py‎

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import unittest
22

3-
import textwrap
43
import threading
54
from threading import Thread
65
import time
76
from unittest import TestCase
87
import gc
98

10-
from test import support
11-
from test.support import script_helper, threading_helper
9+
from test.support import threading_helper
1210

1311

1412
class MyObj:
@@ -176,56 +174,6 @@ def reader():
176174
with threading_helper.start_threads(threads):
177175
pass
178176

179-
@support.requires_subprocess()
180-
def test_tight_gc_loop_does_not_starve_attach(self):
181-
script = textwrap.dedent(f"""
182-
import faulthandler
183-
184-
faulthandler.dump_traceback_later({support.SHORT_TIMEOUT}, exit=True)
185-
186-
import gc
187-
import threading
188-
import time
189-
190-
# Add GC-tracked objects to lengthen the stop-the-world pauses.
191-
import abc
192-
import argparse
193-
import collections
194-
import contextlib
195-
import decimal
196-
import enum
197-
import functools
198-
import heapq
199-
import importlib
200-
import inspect
201-
import itertools
202-
import json
203-
import math
204-
import operator
205-
import random
206-
import re
207-
208-
started = threading.Event()
209-
stop = threading.Event()
210-
211-
def collect():
212-
gc.collect()
213-
started.set()
214-
while not stop.is_set():
215-
gc.collect()
216-
217-
thread = threading.Thread(target=collect)
218-
thread.start()
219-
started.wait()
220-
# Each reattachment must make progress between consecutive pauses.
221-
for _ in range(50):
222-
time.sleep(0.02)
223-
stop.set()
224-
thread.join()
225-
faulthandler.cancel_dump_traceback_later()
226-
""")
227-
script_helper.assert_python_ok("-X", "gil=0", "-c", script)
228-
229177

230178
if __name__ == "__main__":
231179
unittest.main()

‎Lib/test/test_free_threading/test_threading.py‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import unittest
2-
from test.support import threading_helper
2+
import textwrap
3+
4+
from test import support
5+
from test.support import script_helper, threading_helper
36

47
threading_helper.requires_working_threading(module=True)
58

@@ -22,5 +25,39 @@ def mutate_thread():
2225
threading_helper.run_concurrently([repr_thread, mutate_thread])
2326

2427

28+
class TestThreadState(unittest.TestCase):
29+
@support.requires_subprocess()
30+
def test_tight_stw_loop_does_not_starve_attach(self):
31+
script = textwrap.dedent(f"""
32+
import faulthandler
33+
34+
faulthandler.dump_traceback_later({support.SHORT_TIMEOUT}, exit=True)
35+
36+
import _testinternalcapi
37+
import threading
38+
import time
39+
40+
started = threading.Event()
41+
stop = threading.Event()
42+
43+
def stop_the_world():
44+
_testinternalcapi.test_stop_the_world()
45+
started.set()
46+
while not stop.is_set():
47+
_testinternalcapi.test_stop_the_world()
48+
49+
thread = threading.Thread(target=stop_the_world)
50+
thread.start()
51+
started.wait()
52+
# Each reattachment must make progress between consecutive pauses.
53+
for _ in range(50):
54+
time.sleep(0.02)
55+
stop.set()
56+
thread.join()
57+
faulthandler.cancel_dump_traceback_later()
58+
""")
59+
script_helper.assert_python_ok("-X", "gil=0", "-c", script)
60+
61+
2562
if __name__ == "__main__":
2663
unittest.main()

‎Modules/_testinternalcapi.c‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,15 @@ get_stack_margin(PyObject *self, PyObject *Py_UNUSED(args))
208208
return PyLong_FromSize_t(_PyOS_STACK_MARGIN_BYTES);
209209
}
210210

211+
static PyObject *
212+
test_stop_the_world(PyObject *self, PyObject *Py_UNUSED(args))
213+
{
214+
PyInterpreterState *interp = _PyInterpreterState_GET();
215+
_PyEval_StopTheWorld(interp);
216+
_PyEval_StartTheWorld(interp);
217+
Py_RETURN_NONE;
218+
}
219+
211220
#ifdef MS_WINDOWS
212221
static const char *
213222
classify_address(uintptr_t addr, int jit_enabled, PyInterpreterState *interp)
@@ -3295,6 +3304,7 @@ static PyMethodDef module_functions[] = {
32953304
{"get_c_recursion_remaining", get_c_recursion_remaining, METH_NOARGS},
32963305
{"get_stack_pointer", get_stack_pointer, METH_NOARGS},
32973306
{"get_stack_margin", get_stack_margin, METH_NOARGS},
3307+
{"test_stop_the_world", test_stop_the_world, METH_NOARGS},
32983308
{"classify_stack_addresses", classify_stack_addresses, METH_VARARGS},
32993309
{"get_jit_code_ranges", get_jit_code_ranges, METH_NOARGS},
33003310
{"get_jit_backend", get_jit_backend, METH_NOARGS},

‎Python/pystate.c‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,6 +2226,22 @@ tstate_try_attach(PyThreadState *tstate)
22262226
#endif
22272227
}
22282228

2229+
static int
2230+
tstate_try_attach_detached(PyThreadState *tstate, int *state)
2231+
{
2232+
#ifdef Py_GIL_DISABLED
2233+
assert(*state == _Py_THREAD_DETACHED ||
2234+
*state == _Py_THREAD_DETACHED_WAITING);
2235+
return _Py_atomic_compare_exchange_int(&tstate->state,
2236+
state,
2237+
_Py_THREAD_ATTACHED);
2238+
#else
2239+
assert(tstate->state == _Py_THREAD_DETACHED);
2240+
tstate->state = _Py_THREAD_ATTACHED;
2241+
return 1;
2242+
#endif
2243+
}
2244+
22292245
static void
22302246
tstate_set_detached(PyThreadState *tstate, int detached_state)
22312247
{
@@ -2264,9 +2280,7 @@ tstate_wait_attach(PyThreadState *tstate)
22642280
else {
22652281
assert(state == _Py_THREAD_DETACHED ||
22662282
state == _Py_THREAD_DETACHED_WAITING);
2267-
if (_Py_atomic_compare_exchange_int(
2268-
&tstate->state, &state, _Py_THREAD_ATTACHED))
2269-
{
2283+
if (tstate_try_attach_detached(tstate, &state)) {
22702284
return;
22712285
}
22722286
}
@@ -2534,9 +2548,12 @@ start_the_world(struct _stoptheworld_state *stw)
25342548
do {
25352549
assert(state == _Py_THREAD_SUSPENDED ||
25362550
state == _Py_THREAD_SUSPENDED_WAITING);
2537-
next_state = (state == _Py_THREAD_SUSPENDED_WAITING
2538-
? _Py_THREAD_DETACHED_WAITING
2539-
: _Py_THREAD_DETACHED);
2551+
if (state == _Py_THREAD_SUSPENDED_WAITING) {
2552+
next_state = _Py_THREAD_DETACHED_WAITING;
2553+
}
2554+
else {
2555+
next_state = _Py_THREAD_DETACHED;
2556+
}
25402557
// Retry if an attach waiter registered concurrently.
25412558
} while (!_Py_atomic_compare_exchange_int(
25422559
&t->state, &state, next_state));

0 commit comments

Comments
 (0)