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`.
:exc:`PythonFinalizationError` during the Python finalization:
* Creating a new Python thread.
+ * :meth:`Joining <threading.Thread.join>` a running daemon thread.
* :func:`os.fork`.
See also the :func:`sys.is_finalizing` function.
.. versionadded:: 3.13
Previously, a plain :exc:`RuntimeError` was raised.
+ .. versionchanged:: next
+
+ :meth:`threading.Thread.join` can now raise this exception.
.. exception:: RecursionError
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 <interpreter shutdown>` :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.
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,
--- /dev/null
+Joining running daemon threads during interpreter shutdown
+now raises :exc:`PythonFinalizationError`.
// 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.