-
-
Notifications
You must be signed in to change notification settings - Fork 30.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gh-126644: Fix various thread safety issues in _interpreters
#126696
Changes from 22 commits
f18939a
756bf41
45c4561
a94eaef
206b581
286e536
2fab7af
446abc1
cc36e8d
e25587e
e4b2c79
aeb483e
92b3ea7
804c41e
2edfda3
bb8feee
5968448
9ab979d
b478375
f393ab6
35323f8
67df772
13e96de
c9e7b08
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import threading | ||
import unittest | ||
|
||
from test import support | ||
from test.support import import_helper | ||
from test.support import threading_helper | ||
_interpreters = import_helper.import_module('_interpreters') | ||
|
||
|
||
@threading_helper.requires_working_threading() | ||
class StressTests(unittest.TestCase): | ||
|
||
@support.requires_resource('cpu') | ||
def test_subinterpreter_thread_safety(self): | ||
# GH-126644: _interpreters had thread safety problems on the free-threaded build | ||
interp = _interpreters.create() | ||
threads = [threading.Thread(target=_interpreters.run_string, args=(interp, "1")) for _ in range(1000)] | ||
threads.extend([threading.Thread(target=_interpreters.destroy, args=(interp,)) for _ in range(1000)]) | ||
# This will spam all kinds of subinterpreter errors, but we don't care. | ||
# We just want to make sure that it doesn't crash. | ||
with threading_helper.start_threads(threads): | ||
pass | ||
|
||
|
||
if __name__ == '__main__': | ||
# Test needs to be a package, so we can do relative imports. | ||
unittest.main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,5 @@ | ||
import os | ||
from test.support import load_package_tests, Py_GIL_DISABLED | ||
import unittest | ||
|
||
if Py_GIL_DISABLED: | ||
raise unittest.SkipTest("GIL disabled") | ||
from test.support import load_package_tests | ||
|
||
def load_tests(*args): | ||
return load_package_tests(os.path.dirname(__file__), *args) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Fix crash when using subinterpreters in multiple threads on the | ||
:term:`free-threaded <free threading>` build. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1047,9 +1047,80 @@ get_main_thread(PyInterpreterState *interp) | |
return _Py_atomic_load_ptr_relaxed(&interp->threads.main); | ||
} | ||
|
||
#define _PyInterpreterState_RUNNING_OK 0 | ||
#define _PyInterpreterState_RUNNING_PREVENTED 1 | ||
|
||
/* | ||
* Is this interpreter allowed to set the main thread anymore? | ||
*/ | ||
int | ||
_PyInterpreterState_IsRunningAllowed(PyInterpreterState *interp) | ||
{ | ||
assert(interp != NULL); | ||
#ifdef Py_GIL_DISABLED | ||
return _Py_atomic_load_int(&interp->threads.prevented) == _PyInterpreterState_RUNNING_OK; | ||
#else | ||
return 1; | ||
#endif | ||
} | ||
|
||
/* | ||
* Atomically prevent future setting of the main thread. | ||
* | ||
* If something goes wrong, then this returns 0 without | ||
* an exception, and 1 otherwise. | ||
* | ||
* On the GIL-ful build, this is a no-op. | ||
*/ | ||
int | ||
_PyInterpreterState_PreventMain(PyInterpreterState *interp) | ||
{ | ||
assert(interp != NULL); | ||
#ifdef Py_GIL_DISABLED | ||
if (_PyInterpreterState_IsRunningMain(interp)) | ||
{ | ||
// Interpreter is running, can't prevent it yet. | ||
return 0; | ||
} | ||
|
||
int expected_prevented = _PyInterpreterState_RUNNING_OK; | ||
if (_Py_atomic_compare_exchange_int(&interp->threads.prevented, | ||
&expected_prevented, | ||
_PyInterpreterState_RUNNING_PREVENTED) == 0) | ||
{ | ||
// Another thread beat us! | ||
return 0; | ||
} | ||
|
||
if (_PyInterpreterState_IsRunningMain(interp)) | ||
{ | ||
// Another thread started right in between the | ||
// prevention period! | ||
|
||
// XXX Is having the prevented flag set a problem for a | ||
// running interpreter? | ||
return 0; | ||
} | ||
|
||
assert(!_PyInterpreterState_IsRunningAllowed(interp)); | ||
assert(!_PyInterpreterState_IsRunningMain(interp)); | ||
return 1; | ||
#else | ||
// The GIL protects us on the default build, so this | ||
// case shouldn't be possible. | ||
assert(!_PyInterpreterState_IsRunningMain(interp)); | ||
return 1; | ||
#endif | ||
} | ||
|
||
int | ||
_PyInterpreterState_SetRunningMain(PyInterpreterState *interp) | ||
{ | ||
if (!_PyInterpreterState_IsRunningAllowed(interp)) | ||
{ | ||
PyErr_SetString(PyExc_RuntimeError, "cannot run this interpreter anymore"); | ||
return -1; | ||
} | ||
if (_PyInterpreterState_FailIfRunningMain(interp) < 0) { | ||
return -1; | ||
} | ||
|
@@ -1513,15 +1584,19 @@ new_threadstate(PyInterpreterState *interp, int whence) | |
PyMem_RawFree(new_tstate); | ||
return NULL; | ||
} | ||
|
||
/* _Py_ReserveTLBCIndex has thread safety issues */ | ||
HEAD_LOCK(runtime); | ||
Comment on lines
+1588
to
+1589
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't seem right. _PyIndexPool_AllocIndex locks internally. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, that's what I thought too. I'm not too sure what's going on here, but this segfaults without this lock. |
||
|
||
int32_t tlbc_idx = _Py_ReserveTLBCIndex(interp); | ||
if (tlbc_idx < 0) { | ||
PyMem_RawFree(new_tstate); | ||
return NULL; | ||
} | ||
#endif | ||
|
||
#else | ||
/* We serialize concurrent creation to protect global state. */ | ||
HEAD_LOCK(runtime); | ||
#endif | ||
|
||
interp->threads.next_unique_id += 1; | ||
uint64_t id = interp->threads.next_unique_id; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't fully understand what you're doing here, but access to the linked list of thread states requires holding
HEAD_LOCK()
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This case is when
_PyInterpreterState_PreventMain
was able to set the prevention flag, but there was a thread that was able to set the main thread right before that happened, so we can't destroy the interpreter while that thread is still alive (but no more threads will be able to set main after its done).