diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index 9197f704fab344..3412d898a3a487 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -1112,7 +1112,7 @@ Cautions regarding runtime finalization In the late stage of :term:`interpreter shutdown`, after attempting to wait for non-daemon threads to exit (though this can be interrupted by :class:`KeyboardInterrupt`) and running the :mod:`atexit` functions, the runtime -is marked as *finalizing*: :c:func:`_Py_IsFinalizing` and +is marked as *finalizing*: :c:func:`Py_IsFinalizing` and :func:`sys.is_finalizing` return true. At this point, only the *finalization thread* that initiated finalization (typically the main thread) is allowed to acquire the :term:`GIL`. diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 319d261ef3fb4d..1ca3888ad31c8e 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -426,6 +426,7 @@ The following exceptions are the exceptions that are usually raised. :exc:`PythonFinalizationError` during the Python finalization: * Creating a new Python thread. + * :meth:`Joining ` a running daemon thread. * :func:`os.fork`. See also the :func:`sys.is_finalizing` function. @@ -433,6 +434,9 @@ The following exceptions are the exceptions that are usually raised. .. versionadded:: 3.13 Previously, a plain :exc:`RuntimeError` was raised. + .. versionchanged:: next + + :meth:`threading.Thread.join` can now raise this exception. .. exception:: RecursionError diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index 00511df32e4388..52f3bdf71c0a02 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -435,6 +435,14 @@ since it is impossible to detect the termination of alien threads. an error to :meth:`~Thread.join` a thread before it has been started and attempts to do so raise the same exception. + If an attempt is made to join a running daemonic thread in in late stages + of :term:`Python finalization ` :meth:`!join` + raises a :exc:`PythonFinalizationError`. + + .. versionchanged:: next + + May raise :exc:`PythonFinalizationError`. + .. attribute:: name A string used for identification purposes only. It has no semantics. diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 214e1ba0b53dd2..cb5686963150db 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1171,6 +1171,77 @@ def __del__(self): self.assertEqual(out.strip(), b"OK") self.assertIn(b"can't create new thread at interpreter shutdown", err) + def test_join_daemon_thread_in_finalization(self): + # gh-123940: Py_Finalize() prevents other threads from running Python + # code, so join() can not succeed unless the thread is already done. + # (Non-Python threads, that is `threading._DummyThread`, can't be + # joined at all.) + # We raise an exception rather than hang. + for timeout in (None, 10): + with self.subTest(timeout=timeout): + code = textwrap.dedent(f""" + import threading + + + def loop(): + while True: + pass + + + class Cycle: + def __init__(self): + self.self_ref = self + self.thr = threading.Thread( + target=loop, daemon=True) + self.thr.start() + + def __del__(self): + assert self.thr.is_alive() + try: + self.thr.join(timeout={timeout}) + except PythonFinalizationError: + assert self.thr.is_alive() + print('got the correct exception!') + + # Cycle holds a reference to itself, which ensures it is + # cleaned up during the GC that runs after daemon threads + # have been forced to exit during finalization. + Cycle() + """) + rc, out, err = assert_python_ok("-c", code) + self.assertEqual(err, b"") + self.assertIn(b"got the correct exception", out) + + def test_join_finished_daemon_thread_in_finalization(self): + # (see previous test) + # If the thread is already finished, join() succeeds. + code = textwrap.dedent(""" + import threading + done = threading.Event() + + def loop(): + done.set() + + + class Cycle: + def __init__(self): + self.self_ref = self + self.thr = threading.Thread(target=loop, daemon=True) + self.thr.start() + done.wait() + + def __del__(self): + assert not self.thr.is_alive() + self.thr.join() + assert not self.thr.is_alive() + print('all clear!') + + Cycle() + """) + rc, out, err = assert_python_ok("-c", code) + self.assertEqual(err, b"") + self.assertIn(b"all clear", out) + 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, diff --git a/Misc/NEWS.d/next/Library/2025-02-21-15-46-43.gh-issue-130402.Rwu_KK.rst b/Misc/NEWS.d/next/Library/2025-02-21-15-46-43.gh-issue-130402.Rwu_KK.rst new file mode 100644 index 00000000000000..b91d429f3f7bf6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-02-21-15-46-43.gh-issue-130402.Rwu_KK.rst @@ -0,0 +1,2 @@ +Joining running daemon threads during interpreter shutdown +now raises :exc:`PythonFinalizationError`. diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index dab6b395cb2e46..802bd7c4feb0b1 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -510,11 +510,21 @@ ThreadHandle_join(ThreadHandle *self, PyTime_t timeout_ns) // To work around this, we set `thread_is_exiting` immediately before // `thread_run` returns. We can be sure that we are not attempting to join // ourselves if the handle's thread is about to exit. - if (!_PyEvent_IsSet(&self->thread_is_exiting) && - ThreadHandle_ident(self) == PyThread_get_thread_ident_ex()) { - // PyThread_join_thread() would deadlock or error out. - PyErr_SetString(ThreadError, "Cannot join current thread"); - return -1; + if (!_PyEvent_IsSet(&self->thread_is_exiting)) { + if (ThreadHandle_ident(self) == PyThread_get_thread_ident_ex()) { + // PyThread_join_thread() would deadlock or error out. + PyErr_SetString(ThreadError, "Cannot join current thread"); + return -1; + } + if (Py_IsFinalizing()) { + // gh-123940: On finalization, other threads are prevented from + // running Python code. They cannot finalize themselves, + // so join() would hang forever (or until timeout). + // We raise instead. + PyErr_SetString(PyExc_PythonFinalizationError, + "cannot join thread at interpreter shutdown"); + return -1; + } } // Wait until the deadline for the thread to exit.