Skip to content

Commit 708ade8

Browse files
committed
Initial commit
1 parent c6e418d commit 708ade8

File tree

4 files changed

+112
-25
lines changed

4 files changed

+112
-25
lines changed

Lib/test/test_thread.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,12 @@ def test_start_duplicate_handle(self):
326326
lock = thread.allocate_lock()
327327
lock.acquire()
328328

329+
handle = thread._ThreadHandle()
329330
def func():
331+
# Simulate the thread bootstrap process.
332+
handle.set_bootstraped()
330333
lock.acquire()
331334

332-
handle = thread._ThreadHandle()
333335
with threading_helper.wait_threads_exit():
334336
thread.start_joinable_thread(func, handle=handle)
335337
with self.assertRaisesRegex(RuntimeError, "thread already started"):

Lib/test/test_threading.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ def f(mutex):
307307
# Issue gh-106236:
308308
with self.assertRaises(RuntimeError):
309309
dummy_thread.join()
310-
dummy_thread._started.clear()
310+
dummy_thread._os_thread_handle._set_done()
311311
with self.assertRaises(RuntimeError):
312312
dummy_thread.is_alive()
313313
# Busy wait for the following condition: after the thread dies, the
@@ -1458,6 +1458,26 @@ def run_in_bg():
14581458
self.assertEqual(err, b"")
14591459
self.assertEqual(out.strip(), b"Exiting...")
14601460

1461+
def test_error_bootstrap(self):
1462+
# gh-140746: Test that Thread.start() doesn't hang indefinitely if
1463+
# the new thread fails (MemoryError in the referenced issue)
1464+
# during its initialization
1465+
1466+
def nothing():
1467+
pass
1468+
1469+
def _set_ident_error():
1470+
1/0
1471+
1472+
with support.catch_unraisable_exception(), self.assertRaises(RuntimeError):
1473+
thread = threading.Thread(target=nothing)
1474+
thread._set_ident = _set_ident_error
1475+
thread.start()
1476+
thread.join()
1477+
self.assertFalse(thread.is_alive())
1478+
self.assertFalse(thread in threading._limbo)
1479+
self.assertFalse(thread in threading._active)
1480+
14611481
class ThreadJoinOnShutdown(BaseTestCase):
14621482

14631483
def _run_and_join(self, script):

Lib/threading.py

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -930,7 +930,6 @@ class is implemented.
930930
if _HAVE_THREAD_NATIVE_ID:
931931
self._native_id = None
932932
self._os_thread_handle = _ThreadHandle()
933-
self._started = Event()
934933
self._initialized = True
935934
# Copy of sys.stderr used by self._invoke_excepthook()
936935
self._stderr = _sys.stderr
@@ -940,7 +939,6 @@ class is implemented.
940939

941940
def _after_fork(self, new_ident=None):
942941
# Private! Called by threading._after_fork().
943-
self._started._at_fork_reinit()
944942
if new_ident is not None:
945943
# This thread is alive.
946944
self._ident = new_ident
@@ -955,7 +953,7 @@ def _after_fork(self, new_ident=None):
955953
def __repr__(self):
956954
assert self._initialized, "Thread.__init__() was not called"
957955
status = "initial"
958-
if self._started.is_set():
956+
if self._os_thread_handle.is_bootstraped():
959957
status = "started"
960958
if self._os_thread_handle.is_done():
961959
status = "stopped"
@@ -978,7 +976,7 @@ def start(self):
978976
if not self._initialized:
979977
raise RuntimeError("thread.__init__() not called")
980978

981-
if self._started.is_set():
979+
if self._os_thread_handle.is_bootstraped():
982980
raise RuntimeError("threads can only be started once")
983981

984982
with _active_limbo_lock:
@@ -999,9 +997,9 @@ def start(self):
999997
daemon=self.daemon)
1000998
except Exception:
1001999
with _active_limbo_lock:
1002-
del _limbo[self]
1000+
_limbo.pop(self, None)
1001+
_active.pop(self._ident, None)
10031002
raise
1004-
self._started.wait() # Will set ident and native_id
10051003

10061004
def run(self):
10071005
"""Method representing the thread's activity.
@@ -1061,7 +1059,7 @@ def _bootstrap_inner(self):
10611059
if _HAVE_THREAD_NATIVE_ID:
10621060
self._set_native_id()
10631061
self._set_os_name()
1064-
self._started.set()
1062+
self._os_thread_handle.set_bootstraped()
10651063
with _active_limbo_lock:
10661064
_active[self._ident] = self
10671065
del _limbo[self]
@@ -1081,11 +1079,7 @@ def _bootstrap_inner(self):
10811079
def _delete(self):
10821080
"Remove current thread from the dict of currently running threads."
10831081
with _active_limbo_lock:
1084-
del _active[get_ident()]
1085-
# There must not be any python code between the previous line
1086-
# and after the lock is released. Otherwise a tracing function
1087-
# could try to acquire the lock again in the same thread, (in
1088-
# current_thread()), and would block.
1082+
_active.pop(self._ident, None)
10891083

10901084
def join(self, timeout=None):
10911085
"""Wait until the thread terminates.
@@ -1113,7 +1107,7 @@ def join(self, timeout=None):
11131107
"""
11141108
if not self._initialized:
11151109
raise RuntimeError("Thread.__init__() not called")
1116-
if not self._started.is_set():
1110+
if not self._os_thread_handle.is_done() and not self._os_thread_handle.is_bootstraped():
11171111
raise RuntimeError("cannot join thread before it is started")
11181112
if self is current_thread():
11191113
raise RuntimeError("cannot join current thread")
@@ -1176,7 +1170,7 @@ def is_alive(self):
11761170
11771171
"""
11781172
assert self._initialized, "Thread.__init__() not called"
1179-
return self._started.is_set() and not self._os_thread_handle.is_done()
1173+
return self._os_thread_handle.is_bootstraped() and not self._os_thread_handle.is_done()
11801174

11811175
@property
11821176
def daemon(self):
@@ -1199,7 +1193,7 @@ def daemon(self, daemonic):
11991193
raise RuntimeError("Thread.__init__() not called")
12001194
if daemonic and not _daemon_threads_allowed():
12011195
raise RuntimeError('daemon threads are disabled in this interpreter')
1202-
if self._started.is_set():
1196+
if self._os_thread_handle.is_bootstraped() or self._os_thread_handle.is_done():
12031197
raise RuntimeError("cannot set daemon status of active thread")
12041198
self._daemonic = daemonic
12051199

@@ -1385,7 +1379,6 @@ class _MainThread(Thread):
13851379

13861380
def __init__(self):
13871381
Thread.__init__(self, name="MainThread", daemon=False)
1388-
self._started.set()
13891382
self._ident = _get_main_thread_ident()
13901383
self._os_thread_handle = _make_thread_handle(self._ident)
13911384
if _HAVE_THREAD_NATIVE_ID:
@@ -1433,7 +1426,6 @@ class _DummyThread(Thread):
14331426
def __init__(self):
14341427
Thread.__init__(self, name=_newname("Dummy-%d"),
14351428
daemon=_daemon_threads_allowed())
1436-
self._started.set()
14371429
self._set_ident()
14381430
self._os_thread_handle = _make_thread_handle(self._ident)
14391431
if _HAVE_THREAD_NATIVE_ID:
@@ -1443,7 +1435,7 @@ def __init__(self):
14431435
_DeleteDummyThreadOnDel(self)
14441436

14451437
def is_alive(self):
1446-
if not self._os_thread_handle.is_done() and self._started.is_set():
1438+
if self._os_thread_handle.is_bootstraped() and not self._os_thread_handle.is_done():
14471439
return True
14481440
raise RuntimeError("thread is not alive")
14491441

Modules/_threadmodule.c

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ typedef enum {
103103
THREAD_HANDLE_NOT_STARTED = 1,
104104
THREAD_HANDLE_STARTING = 2,
105105
THREAD_HANDLE_RUNNING = 3,
106-
THREAD_HANDLE_DONE = 4,
106+
THREAD_HANDLE_FAILED = 4,
107+
THREAD_HANDLE_DONE = 5,
107108
} ThreadHandleState;
108109

109110
// A handle to wait for thread completion.
@@ -139,6 +140,7 @@ typedef struct {
139140

140141
PyMutex mutex;
141142

143+
PyEvent thread_is_bootstraped;
142144
// Set immediately before `thread_run` returns to indicate that the OS
143145
// thread is about to exit. This is used to avoid false positives when
144146
// detecting self-join attempts. See the comment in `ThreadHandle_join()`
@@ -231,6 +233,7 @@ ThreadHandle_new(void)
231233
self->os_handle = 0;
232234
self->has_os_handle = 0;
233235
self->thread_is_exiting = (PyEvent){0};
236+
self->thread_is_bootstraped = (PyEvent){0};
234237
self->mutex = (PyMutex){_Py_UNLOCKED};
235238
self->once = (_PyOnceFlag){0};
236239
self->state = THREAD_HANDLE_NOT_STARTED;
@@ -286,7 +289,8 @@ ThreadHandle_decref(ThreadHandle *self)
286289
// 1. This is the destructor; nothing else holds a reference.
287290
// 2. The refcount going to zero is a "synchronizes-with" event; all
288291
// changes from other threads are visible.
289-
if (self->state == THREAD_HANDLE_RUNNING && !detach_thread(self)) {
292+
if ((self->state == THREAD_HANDLE_RUNNING || self->state == THREAD_HANDLE_FAILED)
293+
&& !detach_thread(self)) {
290294
self->state = THREAD_HANDLE_DONE;
291295
}
292296

@@ -322,6 +326,7 @@ _PyThread_AfterFork(struct _pythread_runtime_state *state)
322326
handle->once = (_PyOnceFlag){_Py_ONCE_INITIALIZED};
323327
handle->mutex = (PyMutex){_Py_UNLOCKED};
324328
_PyEvent_Notify(&handle->thread_is_exiting);
329+
_PyEvent_Notify(&handle->thread_is_bootstraped);
325330
llist_remove(node);
326331
remove_from_shutdown_handles(handle);
327332
}
@@ -393,6 +398,9 @@ thread_run(void *boot_raw)
393398
PyErr_FormatUnraisable(
394399
"Exception ignored in thread started by %R", boot->func);
395400
}
401+
// Notify that the bootstraped is done and failed (e.g. Memory error).
402+
set_thread_handle_state(handle, THREAD_HANDLE_FAILED);
403+
_PyEvent_Notify(&handle->thread_is_bootstraped);
396404
}
397405
else {
398406
Py_DECREF(res);
@@ -502,7 +510,10 @@ static int
502510
join_thread(void *arg)
503511
{
504512
ThreadHandle *handle = (ThreadHandle*)arg;
505-
assert(get_thread_handle_state(handle) == THREAD_HANDLE_RUNNING);
513+
assert(
514+
get_thread_handle_state(handle) == THREAD_HANDLE_RUNNING ||
515+
get_thread_handle_state(handle) == THREAD_HANDLE_FAILED
516+
);
506517
PyThread_handle_t os_handle;
507518
if (ThreadHandle_get_os_handle(handle, &os_handle)) {
508519
int err = 0;
@@ -707,6 +718,46 @@ PyThreadHandleObject_join(PyObject *op, PyObject *args)
707718
Py_RETURN_NONE;
708719
}
709720

721+
static PyObject *
722+
PyThreadHandleObject_is_bootstraped(PyObject *op, PyObject *Py_UNUSED(dummy))
723+
{
724+
PyThreadHandleObject *self = PyThreadHandleObject_CAST(op);
725+
if (_PyEvent_IsSet(&self->handle->thread_is_bootstraped)) {
726+
Py_RETURN_TRUE;
727+
}
728+
else {
729+
Py_RETURN_FALSE;
730+
}
731+
}
732+
733+
static PyObject *
734+
PyThreadHandleObject_wait_bootstraped(PyObject *op, PyObject *Py_UNUSED(dummy))
735+
{
736+
PyThreadHandleObject *self = PyThreadHandleObject_CAST(op);
737+
PyEvent_Wait(&self->handle->thread_is_bootstraped);
738+
Py_RETURN_NONE;
739+
}
740+
741+
static PyObject *
742+
PyThreadHandleObject_set_bootstraped(PyObject *op, PyObject *Py_UNUSED(dummy))
743+
{
744+
PyThreadHandleObject *self = PyThreadHandleObject_CAST(op);
745+
_PyEvent_Notify(&self->handle->thread_is_bootstraped);
746+
Py_RETURN_NONE;
747+
}
748+
749+
static PyObject *
750+
PyThreadHandleObject_is_failed(PyObject *op, PyObject *Py_UNUSED(dummy))
751+
{
752+
PyThreadHandleObject *self = PyThreadHandleObject_CAST(op);
753+
if (get_thread_handle_state(self->handle) == THREAD_HANDLE_FAILED) {
754+
Py_RETURN_TRUE;
755+
}
756+
else {
757+
Py_RETURN_FALSE;
758+
}
759+
}
760+
710761
static PyObject *
711762
PyThreadHandleObject_is_done(PyObject *op, PyObject *Py_UNUSED(dummy))
712763
{
@@ -740,6 +791,10 @@ static PyGetSetDef ThreadHandle_getsetlist[] = {
740791
static PyMethodDef ThreadHandle_methods[] = {
741792
{"join", PyThreadHandleObject_join, METH_VARARGS, NULL},
742793
{"_set_done", PyThreadHandleObject_set_done, METH_NOARGS, NULL},
794+
{"wait_bootstraped", PyThreadHandleObject_wait_bootstraped, METH_NOARGS, NULL},
795+
{"set_bootstraped", PyThreadHandleObject_set_bootstraped, METH_NOARGS, NULL},
796+
{"is_bootstraped", PyThreadHandleObject_is_bootstraped, METH_NOARGS, NULL},
797+
{"is_failed", PyThreadHandleObject_is_failed, METH_NOARGS, NULL},
743798
{"is_done", PyThreadHandleObject_is_done, METH_NOARGS, NULL},
744799
{0, 0}
745800
};
@@ -2025,8 +2080,8 @@ thread_PyThread_start_joinable_thread(PyObject *module, PyObject *fargs,
20252080
hobj) < 0) {
20262081
return NULL;
20272082
}
2028-
2029-
if (hobj == Py_None) {
2083+
int no_handle_arg = (hobj == Py_None);
2084+
if (no_handle_arg) {
20302085
hobj = (PyObject *)PyThreadHandleObject_new(state->thread_handle_type);
20312086
if (hobj == NULL) {
20322087
return NULL;
@@ -2047,6 +2102,23 @@ thread_PyThread_start_joinable_thread(PyObject *module, PyObject *fargs,
20472102
Py_DECREF(hobj);
20482103
return NULL;
20492104
}
2105+
2106+
// gh-140746: catch error before thread really start
2107+
PyThreadHandleObject *thread_handle = PyThreadHandleObject_CAST(hobj);
2108+
if (no_handle_arg) {
2109+
// If the handle is created by this function, we can be sure that
2110+
// the thread is not started before this point. Here, we simulate
2111+
// the thread bootstrap process.
2112+
_PyEvent_Notify(&thread_handle->handle->thread_is_bootstraped);
2113+
}
2114+
PyEvent_Wait(&thread_handle->handle->thread_is_bootstraped);
2115+
2116+
if (get_thread_handle_state(thread_handle->handle) == THREAD_HANDLE_FAILED) {
2117+
PyErr_SetString(ThreadError, "call to/in _bootstrap/_bootstrap_inner failed");
2118+
Py_DECREF(hobj);
2119+
return NULL;
2120+
}
2121+
20502122
return (PyObject *) hobj;
20512123
}
20522124

@@ -2467,6 +2539,7 @@ thread__make_thread_handle(PyObject *module, PyObject *identobj)
24672539
hobj->handle->ident = ident;
24682540
hobj->handle->state = THREAD_HANDLE_RUNNING;
24692541
PyMutex_Unlock(&hobj->handle->mutex);
2542+
_PyEvent_Notify(&hobj->handle->thread_is_bootstraped);
24702543
return (PyObject*) hobj;
24712544
}
24722545

0 commit comments

Comments
 (0)