Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Doc/library/threading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,10 @@ since it is impossible to detect the termination of alien threads.
of control.

This method will raise a :exc:`RuntimeError` if called more than once
on the same thread object.
on the same thread object. It also raises :exc:`RuntimeError` if the
new thread terminates before it has fully started, which can happen
under memory pressure (for example when a :exc:`MemoryError` occurs
while the thread is bootstrapping).

If supported, set the operating system thread name to
:attr:`threading.Thread.name`. The name can be truncated depending on the
Expand All @@ -576,6 +579,10 @@ since it is impossible to detect the termination of alien threads.
.. versionchanged:: 3.14
Set the operating system thread name.

.. versionchanged:: next
:exc:`RuntimeError` is now raised if the thread dies before it has
fully started; previously this method could hang forever.

.. method:: run()

Method representing the thread's activity.
Expand Down
49 changes: 49 additions & 0 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,55 @@ def run_in_bg():
self.assertEqual(err, b"")
self.assertEqual(out.strip(), b"Exiting...")

def test_start_thread_dies_in_bootstrap(self):
# gh-140746: if the newly spawned thread dies before signalling
# that it has started (e.g. a MemoryError while calling _bootstrap),
# Thread.start() must raise instead of hanging forever.
class BrokenBootstrapThread(threading.Thread):
def _bootstrap(self):
# Simulates a failure before self._started.set(): the
# C-level thread_run() catches the exception and the
# thread exits without ever signalling startup.
raise MemoryError('out of memory during bootstrap')

t = BrokenBootstrapThread(target=lambda: None)
with support.catch_unraisable_exception():
with self.assertRaisesRegex(RuntimeError,
"thread failed to start"):
t.start()
self.assertFalse(t.is_alive())
self.assertNotIn(t, threading._limbo)
self.assertNotIn(t, threading.enumerate())

def test_start_thread_dies_in_bootstrap_inner(self):
# gh-140746: same as above with the failure inside
# _bootstrap_inner(), before self._started.set().
t = threading.Thread(target=lambda: None)
with (support.catch_unraisable_exception(),
mock.patch.object(t, '_set_ident',
side_effect=MemoryError('no memory'))):
with self.assertRaisesRegex(RuntimeError,
"thread failed to start"):
t.start()
self.assertFalse(t.is_alive())
self.assertNotIn(t, threading._limbo)
self.assertNotIn(t, threading.enumerate())

def test_start_thread_exits_in_bootstrap(self):
# gh-140746: SystemExit is silently ignored by the C-level
# thread_run(); it must not make Thread.start() hang either.
class ExitingBootstrapThread(threading.Thread):
def _bootstrap(self):
raise SystemExit

t = ExitingBootstrapThread(target=lambda: None)
with self.assertRaisesRegex(RuntimeError, "thread failed to start"):
t.start()
self.assertFalse(t.is_alive())
self.assertNotIn(t, threading._limbo)
self.assertNotIn(t, threading.enumerate())


class ThreadJoinOnShutdown(BaseTestCase):

def _run_and_join(self, script):
Expand Down
19 changes: 18 additions & 1 deletion Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,7 +1144,21 @@ def start(self):
with _active_limbo_lock:
del _limbo[self]
raise
self._started.wait() # Will set ident and native_id
# Wait for the thread to signal that it has started (this also
# ensures that ident and native_id are set). Waiting on the
# Python-level _started event alone can hang forever: if the thread
# dies during its bootstrap (e.g. a MemoryError while calling
# _bootstrap), _started is never set. The C-level event is also
# notified when the thread exits, so this wait cannot hang
# (see gh-140746).
self._os_thread_handle._wait_for_started()
if not self._started.is_set():
# The thread terminated before signalling that it started: it
# never ran and never will. Clean up and fail loudly rather
# than returning a dead, half-initialized thread.
with _active_limbo_lock:
del _limbo[self]
raise RuntimeError("thread failed to start")

def run(self):
"""Method representing the thread's activity.
Expand Down Expand Up @@ -1205,6 +1219,9 @@ def _bootstrap_inner(self):
self._set_native_id()
self._set_os_name()
self._started.set()
# Signal the C-level event *after* _started so that a waiter
# woken by it always observes _started as set (gh-140746).
self._os_thread_handle._set_started()
with _active_limbo_lock:
_active[self._ident] = self
del _limbo[self]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix :meth:`threading.Thread.start` hanging forever when the new thread dies
before signalling that it has started, for instance when a
:exc:`MemoryError` is raised during the thread's bootstrap under memory
pressure. :meth:`!Thread.start` now raises :exc:`RuntimeError` in that case
and the dead thread no longer lingers in :func:`threading.enumerate`.
41 changes: 41 additions & 0 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ typedef struct {
// for a more detailed explanation.
PyEvent thread_is_exiting;

// Set by the thread when its Python-level bootstrap has signalled
// startup (see threading.Thread._bootstrap_inner), or on thread exit
// if the bootstrap terminated before doing so. threading.Thread.start()
// waits on this event rather than on the Python-level started event
// alone, so that it cannot hang forever if the thread dies during its
// bootstrap (e.g. a MemoryError, see gh-140746).
PyEvent thread_started;

// Serializes calls to `join` and `set_done`.
_PyOnceFlag once;

Expand Down Expand Up @@ -232,6 +240,7 @@ ThreadHandle_new(void)
self->os_handle = 0;
self->has_os_handle = 0;
self->thread_is_exiting = (PyEvent){0};
self->thread_started = (PyEvent){0};
self->mutex = (PyMutex){_Py_UNLOCKED};
self->once = (_PyOnceFlag){0};
self->state = THREAD_HANDLE_NOT_STARTED;
Expand Down Expand Up @@ -323,6 +332,7 @@ _PyThread_AfterFork(struct _pythread_runtime_state *state)
handle->once = (_PyOnceFlag){_Py_ONCE_INITIALIZED};
handle->mutex = (PyMutex){_Py_UNLOCKED};
_PyEvent_Notify(&handle->thread_is_exiting);
_PyEvent_Notify(&handle->thread_started);
llist_remove(node);
remove_from_shutdown_handles(handle);
}
Expand Down Expand Up @@ -409,6 +419,12 @@ thread_run(void *boot_raw)
// Don't need to wait for this thread anymore
remove_from_shutdown_handles(handle);

// gh-140746: Unblock any thread waiting in Thread.start(). If the
// bootstrap function terminated before signalling startup (e.g. a
// MemoryError while calling it), this is the only notification the
// waiter will get. If startup was already signalled, this is a no-op.
_PyEvent_Notify(&handle->thread_started);

_PyEvent_Notify(&handle->thread_is_exiting);
ThreadHandle_decref(handle);

Expand Down Expand Up @@ -708,6 +724,28 @@ PyThreadHandleObject_join(PyObject *op, PyObject *args)
Py_RETURN_NONE;
}

static PyObject *
PyThreadHandleObject_set_started(PyObject *op, PyObject *Py_UNUSED(dummy))
{
PyThreadHandleObject *self = PyThreadHandleObject_CAST(op);
_PyEvent_Notify(&self->handle->thread_started);
Py_RETURN_NONE;
}

static PyObject *
PyThreadHandleObject_wait_for_started(PyObject *op, PyObject *Py_UNUSED(dummy))
{
PyThreadHandleObject *self = PyThreadHandleObject_CAST(op);
while (!PyEvent_WaitTimed(&self->handle->thread_started, -1,
/*detach=*/1)) {
// Interrupted
if (Py_MakePendingCalls() < 0) {
return NULL;
}
}
Py_RETURN_NONE;
}

static PyObject *
PyThreadHandleObject_is_done(PyObject *op, PyObject *Py_UNUSED(dummy))
{
Expand Down Expand Up @@ -741,6 +779,9 @@ static PyGetSetDef ThreadHandle_getsetlist[] = {
static PyMethodDef ThreadHandle_methods[] = {
{"join", PyThreadHandleObject_join, METH_VARARGS, NULL},
{"_set_done", PyThreadHandleObject_set_done, METH_NOARGS, NULL},
{"_set_started", PyThreadHandleObject_set_started, METH_NOARGS, NULL},
{"_wait_for_started", PyThreadHandleObject_wait_for_started,
METH_NOARGS, NULL},
{"is_done", PyThreadHandleObject_is_done, METH_NOARGS, NULL},
{0, 0}
};
Expand Down
Loading