Skip to content

Commit cec1cce

Browse files
committed
gh-151518: Exercise STW fairness directly
1 parent 09c0d2a commit cec1cce

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,15 +1,13 @@
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
import weakref
109

11-
from test import support
12-
from test.support import script_helper, threading_helper
10+
from test.support import threading_helper
1311

1412

1513
class MyObj:
@@ -210,56 +208,6 @@ def reader():
210208
with threading_helper.start_threads(threads):
211209
pass
212210

213-
@support.requires_subprocess()
214-
def test_tight_gc_loop_does_not_starve_attach(self):
215-
script = textwrap.dedent(f"""
216-
import faulthandler
217-
218-
faulthandler.dump_traceback_later({support.SHORT_TIMEOUT}, exit=True)
219-
220-
import gc
221-
import threading
222-
import time
223-
224-
# Add GC-tracked objects to lengthen the stop-the-world pauses.
225-
import abc
226-
import argparse
227-
import collections
228-
import contextlib
229-
import decimal
230-
import enum
231-
import functools
232-
import heapq
233-
import importlib
234-
import inspect
235-
import itertools
236-
import json
237-
import math
238-
import operator
239-
import random
240-
import re
241-
242-
started = threading.Event()
243-
stop = threading.Event()
244-
245-
def collect():
246-
gc.collect()
247-
started.set()
248-
while not stop.is_set():
249-
gc.collect()
250-
251-
thread = threading.Thread(target=collect)
252-
thread.start()
253-
started.wait()
254-
# Each reattachment must make progress between consecutive pauses.
255-
for _ in range(50):
256-
time.sleep(0.02)
257-
stop.set()
258-
thread.join()
259-
faulthandler.cancel_dump_traceback_later()
260-
""")
261-
script_helper.assert_python_ok("-X", "gil=0", "-c", script)
262-
263211

264212
if __name__ == "__main__":
265213
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)
@@ -3298,6 +3307,7 @@ static PyMethodDef module_functions[] = {
32983307
{"get_c_recursion_remaining", get_c_recursion_remaining, METH_NOARGS},
32993308
{"get_stack_pointer", get_stack_pointer, METH_NOARGS},
33003309
{"get_stack_margin", get_stack_margin, METH_NOARGS},
3310+
{"test_stop_the_world", test_stop_the_world, METH_NOARGS},
33013311
{"classify_stack_addresses", classify_stack_addresses, METH_VARARGS},
33023312
{"get_jit_code_ranges", get_jit_code_ranges, METH_NOARGS},
33033313
{"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
}
@@ -2555,9 +2569,12 @@ start_the_world(struct _stoptheworld_state *stw)
25552569
do {
25562570
assert(state == _Py_THREAD_SUSPENDED ||
25572571
state == _Py_THREAD_SUSPENDED_WAITING);
2558-
next_state = (state == _Py_THREAD_SUSPENDED_WAITING
2559-
? _Py_THREAD_DETACHED_WAITING
2560-
: _Py_THREAD_DETACHED);
2572+
if (state == _Py_THREAD_SUSPENDED_WAITING) {
2573+
next_state = _Py_THREAD_DETACHED_WAITING;
2574+
}
2575+
else {
2576+
next_state = _Py_THREAD_DETACHED;
2577+
}
25612578
// Retry if an attach waiter registered concurrently.
25622579
} while (!_Py_atomic_compare_exchange_int(
25632580
&t->state, &state, next_state));

0 commit comments

Comments
 (0)