From 8f9e6759203f4a32b8a813e697d9d06c9707dfd7 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 18 Sep 2025 10:07:17 -0400 Subject: [PATCH 1/4] Ignore non-daemon threads when KeyboardInterrupt is sent during finalization. --- Modules/_threadmodule.c | 4 +--- Python/pylifecycle.c | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 070732aba860b2..cc8277c5783858 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2429,10 +2429,8 @@ thread_shutdown(PyObject *self, PyObject *args) // Wait for the thread to finish. If we're interrupted, such // as by a ctrl-c we print the error and exit early. if (ThreadHandle_join(handle, -1) < 0) { - PyErr_FormatUnraisable("Exception ignored while joining a thread " - "in _thread._shutdown()"); ThreadHandle_decref(handle); - Py_RETURN_NONE; + return NULL; } ThreadHandle_decref(handle); diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index b930e2e2e43e33..37231889740609 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -3548,6 +3548,27 @@ Py_ExitStatusException(PyStatus status) } +static void +handle_thread_shutdown_exception(PyThreadState *tstate) +{ + assert(tstate != NULL); + assert(_PyErr_Occurred(tstate)); + PyInterpreterState *interp = tstate->interp; + assert(interp->threads.head != NULL); + _PyEval_StopTheWorld(interp); + + // We don't have to worry about locking this because the + // world is stopped. + _Py_FOR_EACH_TSTATE_UNLOCKED(interp, tstate) { + if (tstate->_whence == _PyThreadState_WHENCE_THREADING) { + tstate->_whence = _PyThreadState_WHENCE_THREADING_DAEMON; + } + } + + _PyEval_StartTheWorld(interp); + PyErr_FormatUnraisable("Exception ignored on threading shutdown"); +} + /* Wait until threading._shutdown completes, provided the threading module was imported in the first place. The shutdown routine will wait until all non-daemon @@ -3559,14 +3580,14 @@ wait_for_thread_shutdown(PyThreadState *tstate) PyObject *threading = PyImport_GetModule(&_Py_ID(threading)); if (threading == NULL) { if (_PyErr_Occurred(tstate)) { - PyErr_FormatUnraisable("Exception ignored on threading shutdown"); + handle_thread_shutdown_exception(tstate); } /* else: threading not imported */ return; } result = PyObject_CallMethodNoArgs(threading, &_Py_ID(_shutdown)); if (result == NULL) { - PyErr_FormatUnraisable("Exception ignored on threading shutdown"); + handle_thread_shutdown_exception(tstate); } else { Py_DECREF(result); From 2f5073ce77ec401eb5f7fedf5dc315d9e46ee0b1 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 18 Sep 2025 10:13:05 -0400 Subject: [PATCH 2/4] Add a test case. --- Lib/test/test_threading.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 95b2692cd30186..d916827b8588aa 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -6,7 +6,7 @@ from test.support import threading_helper, requires_subprocess, requires_gil_enabled from test.support import verbose, cpython_only, os_helper from test.support.import_helper import ensure_lazy_imports, import_module -from test.support.script_helper import assert_python_ok, assert_python_failure +from test.support.script_helper import assert_python_ok, assert_python_failure, spawn_python from test.support import force_not_colorized import random @@ -2083,6 +2083,32 @@ def test_dummy_thread_on_interpreter_shutdown(self): self.assertEqual(out, b"") self.assertEqual(err, b"") + @requires_subprocess() + def test_keyboard_interrupt_during_threading_shutdown(self): + import subprocess + source = f""" + from threading import Thread + import time + import os + + + def test(): + print('a', flush=True, end='') + time.sleep(10) + + + for _ in range(3): + Thread(target=test).start() + """ + + with spawn_python("-c", source, stderr=subprocess.PIPE) as proc: + self.assertEqual(proc.stdout.read(3), b'aaa') + proc.send_signal(signal.SIGINT) + proc.stderr.flush() + error = proc.stderr.read() + self.assertIn(b"KeyboardInterrupt", error) + self.assertIn(b"threading shutdown", error) + class ThreadRunFail(threading.Thread): def run(self): From 34b8b7ebd83de1c9dd583f47d8b24d0b418e628d Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 18 Sep 2025 10:21:19 -0400 Subject: [PATCH 3/4] Remove flaky part of test. --- Lib/test/test_threading.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index d916827b8588aa..876e2c32228a13 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2107,7 +2107,6 @@ def test(): proc.stderr.flush() error = proc.stderr.read() self.assertIn(b"KeyboardInterrupt", error) - self.assertIn(b"threading shutdown", error) class ThreadRunFail(threading.Thread): From 3455f0535089a8f7c5b06132e1cedcaa90172748 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 18 Sep 2025 11:14:15 -0400 Subject: [PATCH 4/4] SKip the test on Windows. --- Lib/test/test_threading.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 876e2c32228a13..d0f0e8ab2f7724 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2084,6 +2084,7 @@ def test_dummy_thread_on_interpreter_shutdown(self): self.assertEqual(err, b"") @requires_subprocess() + @unittest.skipIf(os.name == 'nt', "signals don't work well on windows") def test_keyboard_interrupt_during_threading_shutdown(self): import subprocess source = f"""