diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 329767aa82e336..7ee0b2964bdb53 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1171,6 +1171,41 @@ def __del__(self): self.assertEqual(out.strip(), b"OK") 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): def _run_and_join(self, script): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst b/Misc/NEWS.d/next/Core_and_Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst new file mode 100644 index 00000000000000..2d350c33aa6975 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst @@ -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. diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 6183d0608b92b6..15b2bd3573c044 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -421,6 +421,7 @@ ThreadHandle_start(ThreadHandle *self, PyObject *func, PyObject *args, PyThread_handle_t os_handle; if (PyThread_start_joinable_thread(thread_run, boot, &ident, &os_handle)) { PyThreadState_Clear(boot->tstate); + PyThreadState_Delete(boot->tstate); thread_bootstate_free(boot, 1); PyErr_SetString(ThreadError, "can't start new thread"); goto start_failed; diff --git a/Python/pystate.c b/Python/pystate.c index 960895e5badc27..528847d80169bf 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1811,7 +1811,9 @@ tstate_delete_common(PyThreadState *tstate, int release_gil) if (tstate->_status.bound_gilstate) { unbind_gilstate_tstate(tstate); } - unbind_tstate(tstate); + if (tstate->_status.bound) { + unbind_tstate(tstate); + } // XXX Move to PyThreadState_Clear()? clear_datastack(tstate);