[3.13] gh-109746: Make _thread.start_new_thread delete state of new thread on its startup failure (GH-109761) (GH-127171)

If Python fails to start newly created thread
due to failure of underlying PyThread_start_new_thread() call,
its state should be removed from interpreter' thread states list
to avoid its double cleanup.

(cherry picked from commit ca3ea9ad05)

Co-authored-by: Radislav Chugunov <52372310+chgnrdv@users.noreply.github.com>
This commit is contained in:
Serhiy Storchaka 2024-11-22 21:55:44 +02:00 committed by GitHub
parent 950daf8765
commit 75ef92da29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 40 additions and 1 deletions

View file

@ -1171,6 +1171,41 @@ class ThreadTests(BaseTestCase):
self.assertEqual(out.strip(), b"OK") self.assertEqual(out.strip(), b"OK")
self.assertIn(b"can't create new thread at interpreter shutdown", err) self.assertIn(b"can't create new thread at interpreter shutdown", err)
def test_start_new_thread_failed(self):
# gh-109746: if Python fails to start newly created thread
# due to failure of underlying PyThread_start_new_thread() call,
# its state should be removed from interpreter' thread states list
# to avoid its double cleanup
try:
from resource import setrlimit, RLIMIT_NPROC
except ImportError as err:
self.skipTest(err) # RLIMIT_NPROC is specific to Linux and BSD
code = """if 1:
import resource
import _thread
def f():
print("shouldn't be printed")
limits = resource.getrlimit(resource.RLIMIT_NPROC)
[_, hard] = limits
resource.setrlimit(resource.RLIMIT_NPROC, (0, hard))
try:
_thread.start_new_thread(f, ())
except RuntimeError:
print('ok')
else:
print('skip')
"""
_, out, err = assert_python_ok("-u", "-c", code)
out = out.strip()
if out == b'skip':
self.skipTest('RLIMIT_NPROC had no effect; probably superuser')
self.assertEqual(out, b'ok')
self.assertEqual(err, b'')
class ThreadJoinOnShutdown(BaseTestCase): class ThreadJoinOnShutdown(BaseTestCase):
def _run_and_join(self, script): def _run_and_join(self, script):

View file

@ -0,0 +1 @@
If :func:`!_thread.start_new_thread` fails to start a new thread, it deletes its state from interpreter and thus avoids its repeated cleanup on finalization.

View file

@ -421,6 +421,7 @@ ThreadHandle_start(ThreadHandle *self, PyObject *func, PyObject *args,
PyThread_handle_t os_handle; PyThread_handle_t os_handle;
if (PyThread_start_joinable_thread(thread_run, boot, &ident, &os_handle)) { if (PyThread_start_joinable_thread(thread_run, boot, &ident, &os_handle)) {
PyThreadState_Clear(boot->tstate); PyThreadState_Clear(boot->tstate);
PyThreadState_Delete(boot->tstate);
thread_bootstate_free(boot, 1); thread_bootstate_free(boot, 1);
PyErr_SetString(ThreadError, "can't start new thread"); PyErr_SetString(ThreadError, "can't start new thread");
goto start_failed; goto start_failed;

View file

@ -1811,7 +1811,9 @@ tstate_delete_common(PyThreadState *tstate, int release_gil)
if (tstate->_status.bound_gilstate) { if (tstate->_status.bound_gilstate) {
unbind_gilstate_tstate(tstate); unbind_gilstate_tstate(tstate);
} }
unbind_tstate(tstate); if (tstate->_status.bound) {
unbind_tstate(tstate);
}
// XXX Move to PyThreadState_Clear()? // XXX Move to PyThreadState_Clear()?
clear_datastack(tstate); clear_datastack(tstate);