From db6bfcf7d0830b3c6ad6f2e0a7249fa7eef7c467 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 11:00:21 -0600 Subject: [PATCH 01/14] Make code sections more vistually distinct. --- Modules/_threadmodule.c | 69 +++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index cc5396a035018f..5d5f7368638d2a 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -25,7 +25,11 @@ // Forward declarations static struct PyModuleDef thread_module; -// Module state + +/****************/ +/* Module state */ +/****************/ + typedef struct { PyTypeObject *excepthook_type; PyTypeObject *lock_type; @@ -42,7 +46,10 @@ get_thread_state(PyObject *module) return (thread_module_state *)state; } -// _ThreadHandle type + +/**********************/ +/* _ThreadHandle type */ +/**********************/ // Handles transition from RUNNING to one of JOINED, DETACHED, or INVALID (post // fork). @@ -267,7 +274,10 @@ static PyType_Spec ThreadHandle_Type_spec = { ThreadHandle_Type_slots, }; + +/****************/ /* Lock objects */ +/****************/ typedef struct { PyObject_HEAD @@ -519,7 +529,33 @@ static PyType_Spec lock_type_spec = { .slots = lock_type_slots, }; +static lockobject * +newlockobject(PyObject *module) +{ + thread_module_state *state = get_thread_state(module); + + PyTypeObject *type = state->lock_type; + lockobject *self = (lockobject *)type->tp_alloc(type, 0); + if (self == NULL) { + return NULL; + } + + self->lock_lock = PyThread_allocate_lock(); + self->locked = 0; + self->in_weakreflist = NULL; + + if (self->lock_lock == NULL) { + Py_DECREF(self); + PyErr_SetString(ThreadError, "can't allocate lock"); + return NULL; + } + return self; +} + + +/**************************/ /* Recursive lock objects */ +/**************************/ typedef struct { PyObject_HEAD @@ -829,30 +865,10 @@ static PyType_Spec rlock_type_spec = { .slots = rlock_type_slots, }; -static lockobject * -newlockobject(PyObject *module) -{ - thread_module_state *state = get_thread_state(module); - - PyTypeObject *type = state->lock_type; - lockobject *self = (lockobject *)type->tp_alloc(type, 0); - if (self == NULL) { - return NULL; - } - - self->lock_lock = PyThread_allocate_lock(); - self->locked = 0; - self->in_weakreflist = NULL; - - if (self->lock_lock == NULL) { - Py_DECREF(self); - PyErr_SetString(ThreadError, "can't allocate lock"); - return NULL; - } - return self; -} +/************************/ /* Thread-local objects */ +/************************/ /* Quick overview: @@ -1270,7 +1286,10 @@ _localdummy_destroyed(PyObject *localweakref, PyObject *dummyweakref) Py_RETURN_NONE; } + +/********************/ /* Module functions */ +/********************/ // bootstate is used to "bootstrap" new threads. Any arguments needed by // `thread_run()`, which can only take a single argument due to platform @@ -1956,7 +1975,9 @@ static PyMethodDef thread_methods[] = { }; +/***************************/ /* Initialization function */ +/***************************/ static int thread_module_exec(PyObject *module) From db48323e13034d3d5f0e9c9e17d2e10a09fe5eeb Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 11:18:13 -0600 Subject: [PATCH 02/14] Combine thread execution code into its own section. --- Modules/_threadmodule.c | 139 ++++++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 62 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 5d5f7368638d2a..ba8318dfe77fea 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1288,7 +1288,7 @@ _localdummy_destroyed(PyObject *localweakref, PyObject *dummyweakref) /********************/ -/* Module functions */ +/* thread execution */ /********************/ // bootstate is used to "bootstrap" new threads. Any arguments needed by @@ -1383,24 +1383,6 @@ thread_run(void *boot_raw) return; } -static PyObject * -thread_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored)) -{ - PyInterpreterState *interp = _PyInterpreterState_GET(); - if (interp->feature_flags & Py_RTFLAGS_DAEMON_THREADS) { - Py_RETURN_TRUE; - } - else { - Py_RETURN_FALSE; - } -} - -PyDoc_STRVAR(daemon_threads_allowed_doc, -"daemon_threads_allowed()\n\ -\n\ -Return True if daemon threads are allowed in the current interpreter,\n\ -and False otherwise.\n"); - static int do_start_new_thread(thread_module_state* state, PyObject *func, PyObject* args, PyObject* kwargs, @@ -1461,6 +1443,77 @@ do_start_new_thread(thread_module_state* state, return 0; } + +static void +release_sentinel(void *weakref_raw) +{ + PyObject *weakref = _PyObject_CAST(weakref_raw); + + /* Tricky: this function is called when the current thread state + is being deleted. Therefore, only simple C code can safely + execute here. */ + lockobject *lock = (lockobject *)_PyWeakref_GET_REF(weakref); + if (lock != NULL) { + if (lock->locked) { + lock->locked = 0; + PyThread_release_lock(lock->lock_lock); + } + Py_DECREF(lock); + } + + /* Deallocating a weakref with a NULL callback only calls + PyObject_GC_Del(), which can't call any Python code. */ + Py_DECREF(weakref); +} + +static int +set_threadstate_finalizer(PyThreadState *tstate, PyObject *lock) +{ + PyObject *wr; + if (tstate->on_delete_data != NULL) { + /* We must support the re-creation of the lock from a + fork()ed child. */ + assert(tstate->on_delete == &release_sentinel); + wr = (PyObject *) tstate->on_delete_data; + tstate->on_delete = NULL; + tstate->on_delete_data = NULL; + Py_DECREF(wr); + } + + /* The lock is owned by whoever called set_threadstate_finalizer(), + but the weakref clings to the thread state. */ + wr = PyWeakref_NewRef(lock, NULL); + if (wr == NULL) { + return -1; + } + tstate->on_delete_data = (void *) wr; + tstate->on_delete = &release_sentinel; + return 0; +} + + +/********************/ +/* Module functions */ +/********************/ + +static PyObject * +thread_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + PyInterpreterState *interp = _PyInterpreterState_GET(); + if (interp->feature_flags & Py_RTFLAGS_DAEMON_THREADS) { + Py_RETURN_TRUE; + } + else { + Py_RETURN_FALSE; + } +} + +PyDoc_STRVAR(daemon_threads_allowed_doc, +"daemon_threads_allowed()\n\ +\n\ +Return True if daemon threads are allowed in the current interpreter,\n\ +and False otherwise.\n"); + static PyObject * thread_PyThread_start_new_thread(PyObject *module, PyObject *fargs) { @@ -1669,57 +1722,19 @@ yet finished.\n\ This function is meant for internal and specialized purposes only.\n\ In most applications `threading.enumerate()` should be used instead."); -static void -release_sentinel(void *weakref_raw) -{ - PyObject *weakref = _PyObject_CAST(weakref_raw); - - /* Tricky: this function is called when the current thread state - is being deleted. Therefore, only simple C code can safely - execute here. */ - lockobject *lock = (lockobject *)_PyWeakref_GET_REF(weakref); - if (lock != NULL) { - if (lock->locked) { - lock->locked = 0; - PyThread_release_lock(lock->lock_lock); - } - Py_DECREF(lock); - } - - /* Deallocating a weakref with a NULL callback only calls - PyObject_GC_Del(), which can't call any Python code. */ - Py_DECREF(weakref); -} - static PyObject * thread__set_sentinel(PyObject *module, PyObject *Py_UNUSED(ignored)) { - PyObject *wr; PyThreadState *tstate = _PyThreadState_GET(); - lockobject *lock; - - if (tstate->on_delete_data != NULL) { - /* We must support the re-creation of the lock from a - fork()ed child. */ - assert(tstate->on_delete == &release_sentinel); - wr = (PyObject *) tstate->on_delete_data; - tstate->on_delete = NULL; - tstate->on_delete_data = NULL; - Py_DECREF(wr); - } - lock = newlockobject(module); - if (lock == NULL) + PyObject *lock = (PyObject *)newlockobject(module); + if (lock == NULL) { return NULL; - /* The lock is owned by whoever called _set_sentinel(), but the weakref - hangs to the thread state. */ - wr = PyWeakref_NewRef((PyObject *) lock, NULL); - if (wr == NULL) { + } + if (set_threadstate_finalizer(tstate, lock) < 0) { Py_DECREF(lock); return NULL; } - tstate->on_delete_data = (void *) wr; - tstate->on_delete = &release_sentinel; - return (PyObject *) lock; + return lock; } PyDoc_STRVAR(_set_sentinel_doc, From 79c1d8038462337cef070f26fddce8ee1c785fa5 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 11:22:41 -0600 Subject: [PATCH 03/14] Make module function names more distinct. --- Modules/_threadmodule.c | 58 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index ba8318dfe77fea..2a81a382f18e48 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1497,7 +1497,7 @@ set_threadstate_finalizer(PyThreadState *tstate, PyObject *lock) /********************/ static PyObject * -thread_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored)) +threadmod_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored)) { PyInterpreterState *interp = _PyInterpreterState_GET(); if (interp->feature_flags & Py_RTFLAGS_DAEMON_THREADS) { @@ -1515,7 +1515,7 @@ Return True if daemon threads are allowed in the current interpreter,\n\ and False otherwise.\n"); static PyObject * -thread_PyThread_start_new_thread(PyObject *module, PyObject *fargs) +threadmod_start_new_thread(PyObject *module, PyObject *fargs) { PyObject *func, *args, *kwargs = NULL; thread_module_state *state = get_thread_state(module); @@ -1567,7 +1567,7 @@ unhandled exception; a stack trace will be printed unless the exception\n\ is SystemExit.\n"); static PyObject * -thread_PyThread_start_joinable_thread(PyObject *module, PyObject *func) +threadmod_start_joinable_thread(PyObject *module, PyObject *func) { thread_module_state *state = get_thread_state(module); @@ -1613,7 +1613,7 @@ This function is not for third-party code, please use the\n\ `threading` module instead.\n"); static PyObject * -thread_PyThread_exit_thread(PyObject *self, PyObject *Py_UNUSED(ignored)) +threadmod_exit_thread(PyObject *self, PyObject *Py_UNUSED(ignored)) { PyErr_SetNone(PyExc_SystemExit); return NULL; @@ -1627,7 +1627,7 @@ This is synonymous to ``raise SystemExit''. It will cause the current\n\ thread to exit silently unless the exception is caught."); static PyObject * -thread_PyThread_interrupt_main(PyObject *self, PyObject *args) +threadmod_interrupt_main(PyObject *self, PyObject *args) { int signum = SIGINT; if (!PyArg_ParseTuple(args, "|i:signum", &signum)) { @@ -1653,7 +1653,7 @@ Note: the default signal handler for SIGINT raises ``KeyboardInterrupt``." ); static PyObject * -thread_PyThread_allocate_lock(PyObject *module, PyObject *Py_UNUSED(ignored)) +threadmod_allocate_lock(PyObject *module, PyObject *Py_UNUSED(ignored)) { return (PyObject *) newlockobject(module); } @@ -1666,7 +1666,7 @@ Create a new lock object. See help(type(threading.Lock())) for\n\ information about locks."); static PyObject * -thread_get_ident(PyObject *self, PyObject *Py_UNUSED(ignored)) +threadmod_get_ident(PyObject *self, PyObject *Py_UNUSED(ignored)) { PyThread_ident_t ident = PyThread_get_thread_ident_ex(); if (ident == PYTHREAD_INVALID_THREAD_ID) { @@ -1689,7 +1689,7 @@ A thread's identity may be reused for another thread after it exits."); #ifdef PY_HAVE_THREAD_NATIVE_ID static PyObject * -thread_get_native_id(PyObject *self, PyObject *Py_UNUSED(ignored)) +threadmod_get_native_id(PyObject *self, PyObject *Py_UNUSED(ignored)) { unsigned long native_id = PyThread_get_thread_native_id(); return PyLong_FromUnsignedLong(native_id); @@ -1704,7 +1704,7 @@ particular thread within a system."); #endif static PyObject * -thread__count(PyObject *self, PyObject *Py_UNUSED(ignored)) +threadmod__count(PyObject *self, PyObject *Py_UNUSED(ignored)) { PyInterpreterState *interp = _PyInterpreterState_GET(); return PyLong_FromSsize_t(_Py_atomic_load_ssize(&interp->threads.count)); @@ -1723,7 +1723,7 @@ This function is meant for internal and specialized purposes only.\n\ In most applications `threading.enumerate()` should be used instead."); static PyObject * -thread__set_sentinel(PyObject *module, PyObject *Py_UNUSED(ignored)) +threadmod__set_sentinel(PyObject *module, PyObject *Py_UNUSED(ignored)) { PyThreadState *tstate = _PyThreadState_GET(); PyObject *lock = (PyObject *)newlockobject(module); @@ -1746,7 +1746,7 @@ state is finalized (after it is untied from the interpreter).\n\ This is a private API for the threading module."); static PyObject * -thread_stack_size(PyObject *self, PyObject *args) +threadmod_stack_size(PyObject *self, PyObject *args) { size_t old_size; Py_ssize_t new_size = 0; @@ -1878,7 +1878,7 @@ static PyStructSequence_Desc ExceptHookArgs_desc = { static PyObject * -thread_excepthook(PyObject *module, PyObject *args) +threadmod_excepthook(PyObject *module, PyObject *args) { thread_module_state *state = get_thread_state(module); @@ -1940,7 +1940,7 @@ PyDoc_STRVAR(excepthook_doc, Handle uncaught Thread.run() exception."); static PyObject * -thread__is_main_interpreter(PyObject *module, PyObject *Py_UNUSED(ignored)) +threadmod__is_main_interpreter(PyObject *module, PyObject *Py_UNUSED(ignored)) { PyInterpreterState *interp = _PyInterpreterState_GET(); return PyBool_FromLong(_Py_IsMainInterpreter(interp)); @@ -1952,39 +1952,39 @@ PyDoc_STRVAR(thread__is_main_interpreter_doc, Return True if the current interpreter is the main Python interpreter."); static PyMethodDef thread_methods[] = { - {"start_new_thread", (PyCFunction)thread_PyThread_start_new_thread, + {"start_new_thread", (PyCFunction)threadmod_start_new_thread, METH_VARARGS, start_new_doc}, - {"start_new", (PyCFunction)thread_PyThread_start_new_thread, + {"start_new", (PyCFunction)threadmod_start_new_thread, METH_VARARGS, start_new_doc}, - {"start_joinable_thread", (PyCFunction)thread_PyThread_start_joinable_thread, + {"start_joinable_thread", (PyCFunction)threadmod_start_joinable_thread, METH_O, start_joinable_doc}, - {"daemon_threads_allowed", (PyCFunction)thread_daemon_threads_allowed, + {"daemon_threads_allowed", (PyCFunction)threadmod_daemon_threads_allowed, METH_NOARGS, daemon_threads_allowed_doc}, - {"allocate_lock", thread_PyThread_allocate_lock, + {"allocate_lock", threadmod_allocate_lock, METH_NOARGS, allocate_doc}, - {"allocate", thread_PyThread_allocate_lock, + {"allocate", threadmod_allocate_lock, METH_NOARGS, allocate_doc}, - {"exit_thread", thread_PyThread_exit_thread, + {"exit_thread", threadmod_exit_thread, METH_NOARGS, exit_doc}, - {"exit", thread_PyThread_exit_thread, + {"exit", threadmod_exit_thread, METH_NOARGS, exit_doc}, - {"interrupt_main", (PyCFunction)thread_PyThread_interrupt_main, + {"interrupt_main", (PyCFunction)threadmod_interrupt_main, METH_VARARGS, interrupt_doc}, - {"get_ident", thread_get_ident, + {"get_ident", threadmod_get_ident, METH_NOARGS, get_ident_doc}, #ifdef PY_HAVE_THREAD_NATIVE_ID - {"get_native_id", thread_get_native_id, + {"get_native_id", threadmod_get_native_id, METH_NOARGS, get_native_id_doc}, #endif - {"_count", thread__count, + {"_count", threadmod__count, METH_NOARGS, _count_doc}, - {"stack_size", (PyCFunction)thread_stack_size, + {"stack_size", (PyCFunction)threadmod_stack_size, METH_VARARGS, stack_size_doc}, - {"_set_sentinel", thread__set_sentinel, + {"_set_sentinel", threadmod__set_sentinel, METH_NOARGS, _set_sentinel_doc}, - {"_excepthook", thread_excepthook, + {"_excepthook", threadmod_excepthook, METH_O, excepthook_doc}, - {"_is_main_interpreter", thread__is_main_interpreter, + {"_is_main_interpreter", threadmod__is_main_interpreter, METH_NOARGS, thread__is_main_interpreter_doc}, {NULL, NULL} /* sentinel */ }; From 75d97c18d3cec12595141c1e4904c6c1f0885028 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 11:46:18 -0600 Subject: [PATCH 04/14] Group the module functions into subsections. --- Modules/_threadmodule.c | 349 +++++++++++++++++++++------------------- 1 file changed, 187 insertions(+), 162 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 2a81a382f18e48..2990cba93a8bfd 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1496,6 +1496,8 @@ set_threadstate_finalizer(PyThreadState *tstate, PyObject *lock) /* Module functions */ /********************/ +/* runtime state inquiries */ + static PyObject * threadmod_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored)) { @@ -1514,117 +1516,83 @@ PyDoc_STRVAR(daemon_threads_allowed_doc, Return True if daemon threads are allowed in the current interpreter,\n\ and False otherwise.\n"); + static PyObject * -threadmod_start_new_thread(PyObject *module, PyObject *fargs) +threadmod__count(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *func, *args, *kwargs = NULL; - thread_module_state *state = get_thread_state(module); - - if (!PyArg_UnpackTuple(fargs, "start_new_thread", 2, 3, - &func, &args, &kwargs)) - return NULL; - if (!PyCallable_Check(func)) { - PyErr_SetString(PyExc_TypeError, - "first arg must be callable"); - return NULL; - } - if (!PyTuple_Check(args)) { - PyErr_SetString(PyExc_TypeError, - "2nd arg must be a tuple"); - return NULL; - } - if (kwargs != NULL && !PyDict_Check(kwargs)) { - PyErr_SetString(PyExc_TypeError, - "optional 3rd arg must be a dictionary"); - return NULL; - } - - if (PySys_Audit("_thread.start_new_thread", "OOO", - func, args, kwargs ? kwargs : Py_None) < 0) { - return NULL; - } - - PyThread_ident_t ident = 0; - PyThread_handle_t handle; - if (do_start_new_thread(state, func, args, kwargs, /*joinable=*/ 0, - &ident, &handle, NULL)) { - return NULL; - } - return PyLong_FromUnsignedLongLong(ident); + PyInterpreterState *interp = _PyInterpreterState_GET(); + return PyLong_FromSsize_t(_Py_atomic_load_ssize(&interp->threads.count)); } -PyDoc_STRVAR(start_new_doc, -"start_new_thread(function, args[, kwargs])\n\ -(start_new() is an obsolete synonym)\n\ +PyDoc_STRVAR(_count_doc, +"_count() -> integer\n\ \n\ -Start a new thread and return its identifier.\n\ +\ +Return the number of currently running Python threads, excluding\n\ +the main thread. The returned number comprises all threads created\n\ +through `start_new_thread()` as well as `threading.Thread`, and not\n\ +yet finished.\n\ \n\ -The thread will call the function with positional arguments from the\n\ -tuple args and keyword arguments taken from the optional dictionary\n\ -kwargs. The thread exits when the function returns; the return value\n\ -is ignored. The thread will also exit when the function raises an\n\ -unhandled exception; a stack trace will be printed unless the exception\n\ -is SystemExit.\n"); +This function is meant for internal and specialized purposes only.\n\ +In most applications `threading.enumerate()` should be used instead."); + static PyObject * -threadmod_start_joinable_thread(PyObject *module, PyObject *func) +threadmod_stack_size(PyObject *self, PyObject *args) { - thread_module_state *state = get_thread_state(module); + size_t old_size; + Py_ssize_t new_size = 0; + int rc; - if (!PyCallable_Check(func)) { - PyErr_SetString(PyExc_TypeError, - "thread function must be callable"); + if (!PyArg_ParseTuple(args, "|n:stack_size", &new_size)) return NULL; - } - if (PySys_Audit("_thread.start_joinable_thread", "O", func) < 0) { + if (new_size < 0) { + PyErr_SetString(PyExc_ValueError, + "size must be 0 or a positive value"); return NULL; } - PyObject* args = PyTuple_New(0); - if (args == NULL) { - return NULL; - } - ThreadHandleObject* hobj = new_thread_handle(state); - if (hobj == NULL) { - Py_DECREF(args); + old_size = PyThread_get_stacksize(); + + rc = PyThread_set_stacksize((size_t) new_size); + if (rc == -1) { + PyErr_Format(PyExc_ValueError, + "size not valid: %zd bytes", + new_size); return NULL; } - if (do_start_new_thread(state, func, args, /*kwargs=*/ NULL, /*joinable=*/ 1, - &hobj->ident, &hobj->handle, hobj->thread_is_exiting)) { - Py_DECREF(args); - Py_DECREF(hobj); + if (rc == -2) { + PyErr_SetString(ThreadError, + "setting stack size not supported"); return NULL; } - set_thread_handle_state(hobj, THREAD_HANDLE_RUNNING); - Py_DECREF(args); - return (PyObject*) hobj; + + return PyLong_FromSsize_t((Py_ssize_t) old_size); } -PyDoc_STRVAR(start_joinable_doc, -"start_joinable_thread(function)\n\ +PyDoc_STRVAR(stack_size_doc, +"stack_size([size]) -> size\n\ \n\ -*For internal use only*: start a new thread.\n\ +Return the thread stack size used when creating new threads. The\n\ +optional size argument specifies the stack size (in bytes) to be used\n\ +for subsequently created threads, and must be 0 (use platform or\n\ +configured default) or a positive integer value of at least 32,768 (32k).\n\ +If changing the thread stack size is unsupported, a ThreadError\n\ +exception is raised. If the specified size is invalid, a ValueError\n\ +exception is raised, and the stack size is unmodified. 32k bytes\n\ + currently the minimum supported stack size value to guarantee\n\ +sufficient stack space for the interpreter itself.\n\ \n\ -Like start_new_thread(), this starts a new thread calling the given function.\n\ -Unlike start_new_thread(), this returns a handle object with methods to join\n\ -or detach the given thread.\n\ -This function is not for third-party code, please use the\n\ -`threading` module instead.\n"); +Note that some platforms may have particular restrictions on values for\n\ +the stack size, such as requiring a minimum stack size larger than 32 KiB or\n\ +requiring allocation in multiples of the system memory page size\n\ +- platform documentation should be referred to for more information\n\ +(4 KiB pages are common; using multiples of 4096 for the stack size is\n\ +the suggested approach in the absence of more specific information)."); -static PyObject * -threadmod_exit_thread(PyObject *self, PyObject *Py_UNUSED(ignored)) -{ - PyErr_SetNone(PyExc_SystemExit); - return NULL; -} -PyDoc_STRVAR(exit_doc, -"exit()\n\ -(exit_thread() is an obsolete synonym)\n\ -\n\ -This is synonymous to ``raise SystemExit''. It will cause the current\n\ -thread to exit silently unless the exception is caught."); +/* signals */ static PyObject * threadmod_interrupt_main(PyObject *self, PyObject *args) @@ -1652,18 +1620,8 @@ A subthread can use this function to interrupt the main thread.\n\ Note: the default signal handler for SIGINT raises ``KeyboardInterrupt``." ); -static PyObject * -threadmod_allocate_lock(PyObject *module, PyObject *Py_UNUSED(ignored)) -{ - return (PyObject *) newlockobject(module); -} -PyDoc_STRVAR(allocate_doc, -"allocate_lock() -> lock object\n\ -(allocate() is an obsolete synonym)\n\ -\n\ -Create a new lock object. See help(type(threading.Lock())) for\n\ -information about locks."); +/* the current OS thread */ static PyObject * threadmod_get_ident(PyObject *self, PyObject *Py_UNUSED(ignored)) @@ -1687,6 +1645,7 @@ allocated consecutive numbers starting at 1, this behavior should not\n\ be relied upon, and the number should be seen purely as a magic cookie.\n\ A thread's identity may be reused for another thread after it exits."); + #ifdef PY_HAVE_THREAD_NATIVE_ID static PyObject * threadmod_get_native_id(PyObject *self, PyObject *Py_UNUSED(ignored)) @@ -1703,101 +1662,162 @@ by the OS (kernel). This may be used to uniquely identify a\n\ particular thread within a system."); #endif + static PyObject * -threadmod__count(PyObject *self, PyObject *Py_UNUSED(ignored)) +threadmod__is_main_interpreter(PyObject *module, PyObject *Py_UNUSED(ignored)) { PyInterpreterState *interp = _PyInterpreterState_GET(); - return PyLong_FromSsize_t(_Py_atomic_load_ssize(&interp->threads.count)); + return PyBool_FromLong(_Py_IsMainInterpreter(interp)); } -PyDoc_STRVAR(_count_doc, -"_count() -> integer\n\ +PyDoc_STRVAR(thread__is_main_interpreter_doc, +"_is_main_interpreter()\n\ \n\ -\ -Return the number of currently running Python threads, excluding\n\ -the main thread. The returned number comprises all threads created\n\ -through `start_new_thread()` as well as `threading.Thread`, and not\n\ -yet finished.\n\ +Return True if the current interpreter is the main Python interpreter."); + + +static PyObject * +threadmod_exit_thread(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + PyErr_SetNone(PyExc_SystemExit); + return NULL; +} + +PyDoc_STRVAR(exit_doc, +"exit()\n\ +(exit_thread() is an obsolete synonym)\n\ \n\ -This function is meant for internal and specialized purposes only.\n\ -In most applications `threading.enumerate()` should be used instead."); +This is synonymous to ``raise SystemExit''. It will cause the current\n\ +thread to exit silently unless the exception is caught."); + + +/* thread execution */ static PyObject * -threadmod__set_sentinel(PyObject *module, PyObject *Py_UNUSED(ignored)) +threadmod_start_new_thread(PyObject *module, PyObject *fargs) { - PyThreadState *tstate = _PyThreadState_GET(); - PyObject *lock = (PyObject *)newlockobject(module); - if (lock == NULL) { + PyObject *func, *args, *kwargs = NULL; + thread_module_state *state = get_thread_state(module); + + if (!PyArg_UnpackTuple(fargs, "start_new_thread", 2, 3, + &func, &args, &kwargs)) + return NULL; + if (!PyCallable_Check(func)) { + PyErr_SetString(PyExc_TypeError, + "first arg must be callable"); return NULL; } - if (set_threadstate_finalizer(tstate, lock) < 0) { - Py_DECREF(lock); + if (!PyTuple_Check(args)) { + PyErr_SetString(PyExc_TypeError, + "2nd arg must be a tuple"); return NULL; } - return lock; + if (kwargs != NULL && !PyDict_Check(kwargs)) { + PyErr_SetString(PyExc_TypeError, + "optional 3rd arg must be a dictionary"); + return NULL; + } + + if (PySys_Audit("_thread.start_new_thread", "OOO", + func, args, kwargs ? kwargs : Py_None) < 0) { + return NULL; + } + + PyThread_ident_t ident = 0; + PyThread_handle_t handle; + if (do_start_new_thread(state, func, args, kwargs, /*joinable=*/ 0, + &ident, &handle, NULL)) { + return NULL; + } + return PyLong_FromUnsignedLongLong(ident); } -PyDoc_STRVAR(_set_sentinel_doc, -"_set_sentinel() -> lock\n\ +PyDoc_STRVAR(start_new_doc, +"start_new_thread(function, args[, kwargs])\n\ +(start_new() is an obsolete synonym)\n\ \n\ -Set a sentinel lock that will be released when the current thread\n\ -state is finalized (after it is untied from the interpreter).\n\ +Start a new thread and return its identifier.\n\ \n\ -This is a private API for the threading module."); +The thread will call the function with positional arguments from the\n\ +tuple args and keyword arguments taken from the optional dictionary\n\ +kwargs. The thread exits when the function returns; the return value\n\ +is ignored. The thread will also exit when the function raises an\n\ +unhandled exception; a stack trace will be printed unless the exception\n\ +is SystemExit.\n"); + static PyObject * -threadmod_stack_size(PyObject *self, PyObject *args) +threadmod_start_joinable_thread(PyObject *module, PyObject *func) { - size_t old_size; - Py_ssize_t new_size = 0; - int rc; + thread_module_state *state = get_thread_state(module); - if (!PyArg_ParseTuple(args, "|n:stack_size", &new_size)) + if (!PyCallable_Check(func)) { + PyErr_SetString(PyExc_TypeError, + "thread function must be callable"); return NULL; + } - if (new_size < 0) { - PyErr_SetString(PyExc_ValueError, - "size must be 0 or a positive value"); + if (PySys_Audit("_thread.start_joinable_thread", "O", func) < 0) { return NULL; } - old_size = PyThread_get_stacksize(); - - rc = PyThread_set_stacksize((size_t) new_size); - if (rc == -1) { - PyErr_Format(PyExc_ValueError, - "size not valid: %zd bytes", - new_size); + PyObject* args = PyTuple_New(0); + if (args == NULL) { return NULL; } - if (rc == -2) { - PyErr_SetString(ThreadError, - "setting stack size not supported"); + ThreadHandleObject* hobj = new_thread_handle(state); + if (hobj == NULL) { + Py_DECREF(args); + return NULL; + } + if (do_start_new_thread(state, func, args, /*kwargs=*/ NULL, /*joinable=*/ 1, + &hobj->ident, &hobj->handle, hobj->thread_is_exiting)) { + Py_DECREF(args); + Py_DECREF(hobj); return NULL; } + set_thread_handle_state(hobj, THREAD_HANDLE_RUNNING); + Py_DECREF(args); + return (PyObject*) hobj; +} - return PyLong_FromSsize_t((Py_ssize_t) old_size); +PyDoc_STRVAR(start_joinable_doc, +"start_joinable_thread(function)\n\ +\n\ +*For internal use only*: start a new thread.\n\ +\n\ +Like start_new_thread(), this starts a new thread calling the given function.\n\ +Unlike start_new_thread(), this returns a handle object with methods to join\n\ +or detach the given thread.\n\ +This function is not for third-party code, please use the\n\ +`threading` module instead.\n"); + + +static PyObject * +threadmod__set_sentinel(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + PyThreadState *tstate = _PyThreadState_GET(); + PyObject *lock = (PyObject *)newlockobject(module); + if (lock == NULL) { + return NULL; + } + if (set_threadstate_finalizer(tstate, lock) < 0) { + Py_DECREF(lock); + return NULL; + } + return lock; } -PyDoc_STRVAR(stack_size_doc, -"stack_size([size]) -> size\n\ +PyDoc_STRVAR(_set_sentinel_doc, +"_set_sentinel() -> lock\n\ \n\ -Return the thread stack size used when creating new threads. The\n\ -optional size argument specifies the stack size (in bytes) to be used\n\ -for subsequently created threads, and must be 0 (use platform or\n\ -configured default) or a positive integer value of at least 32,768 (32k).\n\ -If changing the thread stack size is unsupported, a ThreadError\n\ -exception is raised. If the specified size is invalid, a ValueError\n\ -exception is raised, and the stack size is unmodified. 32k bytes\n\ - currently the minimum supported stack size value to guarantee\n\ -sufficient stack space for the interpreter itself.\n\ +Set a sentinel lock that will be released when the current thread\n\ +state is finalized (after it is untied from the interpreter).\n\ \n\ -Note that some platforms may have particular restrictions on values for\n\ -the stack size, such as requiring a minimum stack size larger than 32 KiB or\n\ -requiring allocation in multiples of the system memory page size\n\ -- platform documentation should be referred to for more information\n\ -(4 KiB pages are common; using multiples of 4096 for the stack size is\n\ -the suggested approach in the absence of more specific information)."); +This is a private API for the threading module."); + + +/* thread excepthook */ static int thread_excepthook_file(PyObject *file, PyObject *exc_type, PyObject *exc_value, @@ -1939,17 +1959,22 @@ PyDoc_STRVAR(excepthook_doc, \n\ Handle uncaught Thread.run() exception."); + +/* other */ + static PyObject * -threadmod__is_main_interpreter(PyObject *module, PyObject *Py_UNUSED(ignored)) +threadmod_allocate_lock(PyObject *module, PyObject *Py_UNUSED(ignored)) { - PyInterpreterState *interp = _PyInterpreterState_GET(); - return PyBool_FromLong(_Py_IsMainInterpreter(interp)); + return (PyObject *) newlockobject(module); } -PyDoc_STRVAR(thread__is_main_interpreter_doc, -"_is_main_interpreter()\n\ +PyDoc_STRVAR(allocate_doc, +"allocate_lock() -> lock object\n\ +(allocate() is an obsolete synonym)\n\ \n\ -Return True if the current interpreter is the main Python interpreter."); +Create a new lock object. See help(type(threading.Lock())) for\n\ +information about locks."); + static PyMethodDef thread_methods[] = { {"start_new_thread", (PyCFunction)threadmod_start_new_thread, From 3cace9ea391c9e67b7cacec7446c92403f3d8f24 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 12:17:29 -0600 Subject: [PATCH 05/14] Factor out thread_bootstate_new(). --- Modules/_threadmodule.c | 146 +++++++++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 56 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 2990cba93a8bfd..ab8f84872dc966 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1302,21 +1302,98 @@ struct bootstate { _PyEventRc *thread_is_exiting; }; +static struct bootstate * +thread_bootstate_new(PyObject *func, PyObject *args, PyObject *kwargs, + _PyEventRc *thread_is_exiting) +{ + PyInterpreterState *interp = _PyInterpreterState_GET(); + if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_THREADS)) { + PyErr_SetString(PyExc_RuntimeError, + "thread is not supported for isolated subinterpreters"); + return NULL; + } + if (interp->finalizing) { + PyErr_SetString(PyExc_PythonFinalizationError, + "can't create new thread at interpreter shutdown"); + return NULL; + } + + PyThreadState *tstate = _PyThreadState_New( + interp, _PyThreadState_WHENCE_THREADING); + if (tstate == NULL) { + if (!PyErr_Occurred()) { + PyErr_NoMemory(); + } + return NULL; + } + + if (args == NULL) { + args = PyTuple_New(0); + if (args == NULL) { + PyThreadState_Clear(tstate); + PyThreadState_Delete(tstate); + } + } + else { + Py_INCREF(args); + } + + // gh-109795: Use PyMem_RawMalloc() instead of PyMem_Malloc(), + // because it should be possible to call thread_bootstate_free() + // without holding the GIL. + struct bootstate *boot = PyMem_RawMalloc(sizeof(struct bootstate)); + if (boot == NULL) { + PyErr_NoMemory(); + Py_DECREF(args); + PyThreadState_Clear(tstate); + PyThreadState_Delete(tstate); + return NULL; + } + *boot = (struct bootstate){ + .tstate = tstate, + .func = Py_NewRef(func), + .args = args, + .kwargs = Py_XNewRef(kwargs), + .thread_is_exiting = thread_is_exiting, + }; + if (thread_is_exiting != NULL) { + _PyEventRc_Incref(thread_is_exiting); + } + return boot; +} static void -thread_bootstate_free(struct bootstate *boot, int decref) +thread_bootstate_free(struct bootstate *boot) { - if (decref) { - Py_DECREF(boot->func); - Py_DECREF(boot->args); - Py_XDECREF(boot->kwargs); - } + Py_XDECREF(boot->func); + Py_XDECREF(boot->args); + Py_XDECREF(boot->kwargs); if (boot->thread_is_exiting != NULL) { _PyEventRc_Decref(boot->thread_is_exiting); } + if (boot->tstate != NULL) { + PyThreadState_Clear(boot->tstate); + // XXX PyThreadState_Delete() too? + } PyMem_RawFree(boot); } +static void +thread_bootstate_finalizing(struct bootstate *boot) +{ + // Don't call PyThreadState_Clear() nor _PyThreadState_DeleteCurrent(). + // These functions are called on tstate indirectly by Py_Finalize() + // which calls _PyInterpreterState_Clear(). + // + // Py_DECREF() cannot be called because the GIL is not held: leak + // references on purpose. Python is being finalized anyway. + boot->tstate = NULL; + boot->func = NULL; + boot->args = NULL; + boot->kwargs = NULL; + thread_bootstate_free(boot); +} + static void thread_run(void *boot_raw) @@ -1337,13 +1414,7 @@ thread_run(void *boot_raw) // At this stage, tstate can be a dangling pointer (point to freed memory), // it's ok to call _PyThreadState_MustExit() with a dangling pointer. if (_PyThreadState_MustExit(tstate)) { - // Don't call PyThreadState_Clear() nor _PyThreadState_DeleteCurrent(). - // These functions are called on tstate indirectly by Py_Finalize() - // which calls _PyInterpreterState_Clear(). - // - // Py_DECREF() cannot be called because the GIL is not held: leak - // references on purpose. Python is being finalized anyway. - thread_bootstate_free(boot, 0); + thread_bootstate_finalizing(boot); goto exit; } @@ -1365,7 +1436,8 @@ thread_run(void *boot_raw) Py_DECREF(res); } - thread_bootstate_free(boot, 1); + boot->tstate = NULL; + thread_bootstate_free(boot); _Py_atomic_add_ssize(&tstate->interp->threads.count, -1); PyThreadState_Clear(tstate); @@ -1390,41 +1462,11 @@ do_start_new_thread(thread_module_state* state, PyThread_ident_t* ident, PyThread_handle_t* handle, _PyEventRc *thread_is_exiting) { - PyInterpreterState *interp = _PyInterpreterState_GET(); - if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_THREADS)) { - PyErr_SetString(PyExc_RuntimeError, - "thread is not supported for isolated subinterpreters"); - return -1; - } - if (interp->finalizing) { - PyErr_SetString(PyExc_PythonFinalizationError, - "can't create new thread at interpreter shutdown"); - return -1; - } - - // gh-109795: Use PyMem_RawMalloc() instead of PyMem_Malloc(), - // because it should be possible to call thread_bootstate_free() - // without holding the GIL. - struct bootstate *boot = PyMem_RawMalloc(sizeof(struct bootstate)); + struct bootstate *boot = thread_bootstate_new( + func, args, kwargs, thread_is_exiting); if (boot == NULL) { - PyErr_NoMemory(); - return -1; - } - boot->tstate = _PyThreadState_New(interp, _PyThreadState_WHENCE_THREADING); - if (boot->tstate == NULL) { - PyMem_RawFree(boot); - if (!PyErr_Occurred()) { - PyErr_NoMemory(); - } return -1; } - boot->func = Py_NewRef(func); - boot->args = Py_NewRef(args); - boot->kwargs = Py_XNewRef(kwargs); - boot->thread_is_exiting = thread_is_exiting; - if (thread_is_exiting != NULL) { - _PyEventRc_Incref(thread_is_exiting); - } int err; if (joinable) { @@ -1436,8 +1478,7 @@ do_start_new_thread(thread_module_state* state, } if (err) { PyErr_SetString(ThreadError, "can't start new thread"); - PyThreadState_Clear(boot->tstate); - thread_bootstate_free(boot, 1); + thread_bootstate_free(boot); return -1; } return 0; @@ -1761,23 +1802,16 @@ threadmod_start_joinable_thread(PyObject *module, PyObject *func) return NULL; } - PyObject* args = PyTuple_New(0); - if (args == NULL) { - return NULL; - } ThreadHandleObject* hobj = new_thread_handle(state); if (hobj == NULL) { - Py_DECREF(args); return NULL; } - if (do_start_new_thread(state, func, args, /*kwargs=*/ NULL, /*joinable=*/ 1, + if (do_start_new_thread(state, func, /* args */ NULL, /*kwargs=*/ NULL, /*joinable=*/ 1, &hobj->ident, &hobj->handle, hobj->thread_is_exiting)) { - Py_DECREF(args); Py_DECREF(hobj); return NULL; } set_thread_handle_state(hobj, THREAD_HANDLE_RUNNING); - Py_DECREF(args); return (PyObject*) hobj; } From 0e7a0f74a999fd5b1c3c15061638a3895b632de0 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 12:36:11 -0600 Subject: [PATCH 06/14] Pass the boot state to do_start_new_thread(). --- Modules/_threadmodule.c | 47 ++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index ab8f84872dc966..3d99f4ddcffadc 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1456,31 +1456,24 @@ thread_run(void *boot_raw) } static int -do_start_new_thread(thread_module_state* state, - PyObject *func, PyObject* args, PyObject* kwargs, - int joinable, - PyThread_ident_t* ident, PyThread_handle_t* handle, - _PyEventRc *thread_is_exiting) +do_start_new_thread(struct bootstate *boot, int joinable, + PyThread_ident_t *p_ident, PyThread_handle_t *p_handle) { - struct bootstate *boot = thread_bootstate_new( - func, args, kwargs, thread_is_exiting); - if (boot == NULL) { - return -1; - } - int err; + PyThread_ident_t ident = 0; + PyThread_handle_t handle = 0; if (joinable) { - err = PyThread_start_joinable_thread(thread_run, (void*) boot, ident, handle); + err = PyThread_start_joinable_thread(thread_run, (void*) boot, &ident, &handle); } else { - *handle = 0; - *ident = PyThread_start_new_thread(thread_run, (void*) boot); - err = (*ident == PYTHREAD_INVALID_THREAD_ID); + ident = PyThread_start_new_thread(thread_run, (void*) boot); + err = (*p_ident == PYTHREAD_INVALID_THREAD_ID); } if (err) { PyErr_SetString(ThreadError, "can't start new thread"); - thread_bootstate_free(boot); return -1; } + *p_ident = ident; + *p_handle = handle; return 0; } @@ -1738,8 +1731,6 @@ static PyObject * threadmod_start_new_thread(PyObject *module, PyObject *fargs) { PyObject *func, *args, *kwargs = NULL; - thread_module_state *state = get_thread_state(module); - if (!PyArg_UnpackTuple(fargs, "start_new_thread", 2, 3, &func, &args, &kwargs)) return NULL; @@ -1764,10 +1755,15 @@ threadmod_start_new_thread(PyObject *module, PyObject *fargs) return NULL; } + struct bootstate *boot = thread_bootstate_new(func, args, kwargs, NULL); + if (boot == NULL) { + return NULL; + } + PyThread_ident_t ident = 0; PyThread_handle_t handle; - if (do_start_new_thread(state, func, args, kwargs, /*joinable=*/ 0, - &ident, &handle, NULL)) { + if (do_start_new_thread(boot, /*joinable=*/ 0, &ident, &handle)) { + thread_bootstate_free(boot); return NULL; } return PyLong_FromUnsignedLongLong(ident); @@ -1806,8 +1802,15 @@ threadmod_start_joinable_thread(PyObject *module, PyObject *func) if (hobj == NULL) { return NULL; } - if (do_start_new_thread(state, func, /* args */ NULL, /*kwargs=*/ NULL, /*joinable=*/ 1, - &hobj->ident, &hobj->handle, hobj->thread_is_exiting)) { + struct bootstate *boot = thread_bootstate_new( + func, NULL, NULL, hobj->thread_is_exiting); + if (boot == NULL) { + Py_DECREF(hobj); + return NULL; + } + if (do_start_new_thread(boot, /*joinable=*/ 1, + &hobj->ident, &hobj->handle)) { + thread_bootstate_free(boot); Py_DECREF(hobj); return NULL; } From cec325e3af5fa79ce94398e615e8093693fc864d Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 13:09:33 -0600 Subject: [PATCH 07/14] Drop do_start_new_thread(). --- Modules/_threadmodule.c | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 3d99f4ddcffadc..5ee1e543bac319 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1455,28 +1455,6 @@ thread_run(void *boot_raw) return; } -static int -do_start_new_thread(struct bootstate *boot, int joinable, - PyThread_ident_t *p_ident, PyThread_handle_t *p_handle) -{ - int err; - PyThread_ident_t ident = 0; - PyThread_handle_t handle = 0; - if (joinable) { - err = PyThread_start_joinable_thread(thread_run, (void*) boot, &ident, &handle); - } else { - ident = PyThread_start_new_thread(thread_run, (void*) boot); - err = (*p_ident == PYTHREAD_INVALID_THREAD_ID); - } - if (err) { - PyErr_SetString(ThreadError, "can't start new thread"); - return -1; - } - *p_ident = ident; - *p_handle = handle; - return 0; -} - static void release_sentinel(void *weakref_raw) @@ -1760,9 +1738,9 @@ threadmod_start_new_thread(PyObject *module, PyObject *fargs) return NULL; } - PyThread_ident_t ident = 0; - PyThread_handle_t handle; - if (do_start_new_thread(boot, /*joinable=*/ 0, &ident, &handle)) { + PyThread_ident_t ident = PyThread_start_new_thread(thread_run, (void*) boot); + if (ident == PYTHREAD_INVALID_THREAD_ID) { + PyErr_SetString(ThreadError, "can't start new thread"); thread_bootstate_free(boot); return NULL; } @@ -1808,8 +1786,10 @@ threadmod_start_joinable_thread(PyObject *module, PyObject *func) Py_DECREF(hobj); return NULL; } - if (do_start_new_thread(boot, /*joinable=*/ 1, - &hobj->ident, &hobj->handle)) { + if (PyThread_start_joinable_thread( + thread_run, (void*) boot, &hobj->ident, &hobj->handle) < 0) + { + PyErr_SetString(ThreadError, "can't start new thread"); thread_bootstate_free(boot); Py_DECREF(hobj); return NULL; From 4c83cf5cd11daae2d0014633df642918189decf3 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 13:26:18 -0600 Subject: [PATCH 08/14] Move the thread execution code up near the top. --- Modules/_threadmodule.c | 443 ++++++++++++++++++++-------------------- 1 file changed, 226 insertions(+), 217 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 5ee1e543bac319..52e98cc286f6b3 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -275,6 +275,222 @@ static PyType_Spec ThreadHandle_Type_spec = { }; +/********************/ +/* thread execution */ +/********************/ + +// bootstate is used to "bootstrap" new threads. Any arguments needed by +// `thread_run()`, which can only take a single argument due to platform +// limitations, are contained in bootstate. +struct bootstate { + PyThreadState *tstate; + PyObject *func; + PyObject *args; + PyObject *kwargs; + _PyEventRc *thread_is_exiting; +}; + +static struct bootstate * +thread_bootstate_new(PyObject *func, PyObject *args, PyObject *kwargs, + _PyEventRc *thread_is_exiting) +{ + PyInterpreterState *interp = _PyInterpreterState_GET(); + if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_THREADS)) { + PyErr_SetString(PyExc_RuntimeError, + "thread is not supported for isolated subinterpreters"); + return NULL; + } + if (interp->finalizing) { + PyErr_SetString(PyExc_PythonFinalizationError, + "can't create new thread at interpreter shutdown"); + return NULL; + } + + PyThreadState *tstate = _PyThreadState_New( + interp, _PyThreadState_WHENCE_THREADING); + if (tstate == NULL) { + if (!PyErr_Occurred()) { + PyErr_NoMemory(); + } + return NULL; + } + + if (args == NULL) { + args = PyTuple_New(0); + if (args == NULL) { + PyThreadState_Clear(tstate); + PyThreadState_Delete(tstate); + } + } + else { + Py_INCREF(args); + } + + // gh-109795: Use PyMem_RawMalloc() instead of PyMem_Malloc(), + // because it should be possible to call thread_bootstate_free() + // without holding the GIL. + struct bootstate *boot = PyMem_RawMalloc(sizeof(struct bootstate)); + if (boot == NULL) { + PyErr_NoMemory(); + Py_DECREF(args); + PyThreadState_Clear(tstate); + PyThreadState_Delete(tstate); + return NULL; + } + *boot = (struct bootstate){ + .tstate = tstate, + .func = Py_NewRef(func), + .args = args, + .kwargs = Py_XNewRef(kwargs), + .thread_is_exiting = thread_is_exiting, + }; + if (thread_is_exiting != NULL) { + _PyEventRc_Incref(thread_is_exiting); + } + return boot; +} + +static void +thread_bootstate_free(struct bootstate *boot) +{ + Py_XDECREF(boot->func); + Py_XDECREF(boot->args); + Py_XDECREF(boot->kwargs); + if (boot->thread_is_exiting != NULL) { + _PyEventRc_Decref(boot->thread_is_exiting); + } + if (boot->tstate != NULL) { + PyThreadState_Clear(boot->tstate); + // XXX PyThreadState_Delete() too? + } + PyMem_RawFree(boot); +} + +static void +thread_bootstate_finalizing(struct bootstate *boot) +{ + // Don't call PyThreadState_Clear() nor _PyThreadState_DeleteCurrent(). + // These functions are called on tstate indirectly by Py_Finalize() + // which calls _PyInterpreterState_Clear(). + // + // Py_DECREF() cannot be called because the GIL is not held: leak + // references on purpose. Python is being finalized anyway. + boot->tstate = NULL; + boot->func = NULL; + boot->args = NULL; + boot->kwargs = NULL; + thread_bootstate_free(boot); +} + + +static void +thread_run(void *boot_raw) +{ + struct bootstate *boot = (struct bootstate *) boot_raw; + PyThreadState *tstate = boot->tstate; + + // `thread_is_exiting` needs to be set after bootstate has been freed + _PyEventRc *thread_is_exiting = boot->thread_is_exiting; + boot->thread_is_exiting = NULL; + + // gh-108987: If _thread.start_new_thread() is called before or while + // Python is being finalized, thread_run() can called *after*. + // _PyRuntimeState_SetFinalizing() is called. At this point, all Python + // threads must exit, except of the thread calling Py_Finalize() whch holds + // the GIL and must not exit. + // + // At this stage, tstate can be a dangling pointer (point to freed memory), + // it's ok to call _PyThreadState_MustExit() with a dangling pointer. + if (_PyThreadState_MustExit(tstate)) { + thread_bootstate_finalizing(boot); + goto exit; + } + + _PyThreadState_Bind(tstate); + PyEval_AcquireThread(tstate); + _Py_atomic_add_ssize(&tstate->interp->threads.count, 1); + + PyObject *res = PyObject_Call(boot->func, boot->args, boot->kwargs); + if (res == NULL) { + if (PyErr_ExceptionMatches(PyExc_SystemExit)) + /* SystemExit is ignored silently */ + PyErr_Clear(); + else { + PyErr_FormatUnraisable( + "Exception ignored in thread started by %R", boot->func); + } + } + else { + Py_DECREF(res); + } + + boot->tstate = NULL; + thread_bootstate_free(boot); + + _Py_atomic_add_ssize(&tstate->interp->threads.count, -1); + PyThreadState_Clear(tstate); + _PyThreadState_DeleteCurrent(tstate); + +exit: + if (thread_is_exiting != NULL) { + _PyEvent_Notify(&thread_is_exiting->event); + _PyEventRc_Decref(thread_is_exiting); + } + + // bpo-44434: Don't call explicitly PyThread_exit_thread(). On Linux with + // the glibc, pthread_exit() can abort the whole process if dlopen() fails + // to open the libgcc_s.so library (ex: EMFILE error). + return; +} + + +static void _lock_unlock(PyObject *); + +static void +release_sentinel(void *weakref_raw) +{ + PyObject *weakref = _PyObject_CAST(weakref_raw); + + /* Tricky: this function is called when the current thread state + is being deleted. Therefore, only simple C code can safely + execute here. */ + PyObject *lock = _PyWeakref_GET_REF(weakref); + if (lock != NULL) { + _lock_unlock(lock); + Py_DECREF(lock); + } + + /* Deallocating a weakref with a NULL callback only calls + PyObject_GC_Del(), which can't call any Python code. */ + Py_DECREF(weakref); +} + +static int +set_threadstate_finalizer(PyThreadState *tstate, PyObject *lock) +{ + PyObject *wr; + if (tstate->on_delete_data != NULL) { + /* We must support the re-creation of the lock from a + fork()ed child. */ + assert(tstate->on_delete == &release_sentinel); + wr = (PyObject *) tstate->on_delete_data; + tstate->on_delete = NULL; + tstate->on_delete_data = NULL; + Py_DECREF(wr); + } + + /* The lock is owned by whoever called set_threadstate_finalizer(), + but the weakref clings to the thread state. */ + wr = PyWeakref_NewRef(lock, NULL); + if (wr == NULL) { + return -1; + } + tstate->on_delete_data = (void *) wr; + tstate->on_delete = &release_sentinel; + return 0; +} + + /****************/ /* Lock objects */ /****************/ @@ -391,6 +607,16 @@ With an argument, this will only block if the argument is true,\n\ and the return value reflects whether the lock is acquired.\n\ The blocking operation is interruptible."); +static void +_lock_unlock(PyObject *obj) +{ + lockobject *lock = (lockobject *)obj; + if (lock->locked) { + lock->locked = 0; + PyThread_release_lock(lock->lock_lock); + } +} + static PyObject * lock_PyThread_release_lock(lockobject *self, PyObject *Py_UNUSED(ignored)) { @@ -1287,223 +1513,6 @@ _localdummy_destroyed(PyObject *localweakref, PyObject *dummyweakref) } -/********************/ -/* thread execution */ -/********************/ - -// bootstate is used to "bootstrap" new threads. Any arguments needed by -// `thread_run()`, which can only take a single argument due to platform -// limitations, are contained in bootstate. -struct bootstate { - PyThreadState *tstate; - PyObject *func; - PyObject *args; - PyObject *kwargs; - _PyEventRc *thread_is_exiting; -}; - -static struct bootstate * -thread_bootstate_new(PyObject *func, PyObject *args, PyObject *kwargs, - _PyEventRc *thread_is_exiting) -{ - PyInterpreterState *interp = _PyInterpreterState_GET(); - if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_THREADS)) { - PyErr_SetString(PyExc_RuntimeError, - "thread is not supported for isolated subinterpreters"); - return NULL; - } - if (interp->finalizing) { - PyErr_SetString(PyExc_PythonFinalizationError, - "can't create new thread at interpreter shutdown"); - return NULL; - } - - PyThreadState *tstate = _PyThreadState_New( - interp, _PyThreadState_WHENCE_THREADING); - if (tstate == NULL) { - if (!PyErr_Occurred()) { - PyErr_NoMemory(); - } - return NULL; - } - - if (args == NULL) { - args = PyTuple_New(0); - if (args == NULL) { - PyThreadState_Clear(tstate); - PyThreadState_Delete(tstate); - } - } - else { - Py_INCREF(args); - } - - // gh-109795: Use PyMem_RawMalloc() instead of PyMem_Malloc(), - // because it should be possible to call thread_bootstate_free() - // without holding the GIL. - struct bootstate *boot = PyMem_RawMalloc(sizeof(struct bootstate)); - if (boot == NULL) { - PyErr_NoMemory(); - Py_DECREF(args); - PyThreadState_Clear(tstate); - PyThreadState_Delete(tstate); - return NULL; - } - *boot = (struct bootstate){ - .tstate = tstate, - .func = Py_NewRef(func), - .args = args, - .kwargs = Py_XNewRef(kwargs), - .thread_is_exiting = thread_is_exiting, - }; - if (thread_is_exiting != NULL) { - _PyEventRc_Incref(thread_is_exiting); - } - return boot; -} - -static void -thread_bootstate_free(struct bootstate *boot) -{ - Py_XDECREF(boot->func); - Py_XDECREF(boot->args); - Py_XDECREF(boot->kwargs); - if (boot->thread_is_exiting != NULL) { - _PyEventRc_Decref(boot->thread_is_exiting); - } - if (boot->tstate != NULL) { - PyThreadState_Clear(boot->tstate); - // XXX PyThreadState_Delete() too? - } - PyMem_RawFree(boot); -} - -static void -thread_bootstate_finalizing(struct bootstate *boot) -{ - // Don't call PyThreadState_Clear() nor _PyThreadState_DeleteCurrent(). - // These functions are called on tstate indirectly by Py_Finalize() - // which calls _PyInterpreterState_Clear(). - // - // Py_DECREF() cannot be called because the GIL is not held: leak - // references on purpose. Python is being finalized anyway. - boot->tstate = NULL; - boot->func = NULL; - boot->args = NULL; - boot->kwargs = NULL; - thread_bootstate_free(boot); -} - - -static void -thread_run(void *boot_raw) -{ - struct bootstate *boot = (struct bootstate *) boot_raw; - PyThreadState *tstate = boot->tstate; - - // `thread_is_exiting` needs to be set after bootstate has been freed - _PyEventRc *thread_is_exiting = boot->thread_is_exiting; - boot->thread_is_exiting = NULL; - - // gh-108987: If _thread.start_new_thread() is called before or while - // Python is being finalized, thread_run() can called *after*. - // _PyRuntimeState_SetFinalizing() is called. At this point, all Python - // threads must exit, except of the thread calling Py_Finalize() whch holds - // the GIL and must not exit. - // - // At this stage, tstate can be a dangling pointer (point to freed memory), - // it's ok to call _PyThreadState_MustExit() with a dangling pointer. - if (_PyThreadState_MustExit(tstate)) { - thread_bootstate_finalizing(boot); - goto exit; - } - - _PyThreadState_Bind(tstate); - PyEval_AcquireThread(tstate); - _Py_atomic_add_ssize(&tstate->interp->threads.count, 1); - - PyObject *res = PyObject_Call(boot->func, boot->args, boot->kwargs); - if (res == NULL) { - if (PyErr_ExceptionMatches(PyExc_SystemExit)) - /* SystemExit is ignored silently */ - PyErr_Clear(); - else { - PyErr_FormatUnraisable( - "Exception ignored in thread started by %R", boot->func); - } - } - else { - Py_DECREF(res); - } - - boot->tstate = NULL; - thread_bootstate_free(boot); - - _Py_atomic_add_ssize(&tstate->interp->threads.count, -1); - PyThreadState_Clear(tstate); - _PyThreadState_DeleteCurrent(tstate); - -exit: - if (thread_is_exiting != NULL) { - _PyEvent_Notify(&thread_is_exiting->event); - _PyEventRc_Decref(thread_is_exiting); - } - - // bpo-44434: Don't call explicitly PyThread_exit_thread(). On Linux with - // the glibc, pthread_exit() can abort the whole process if dlopen() fails - // to open the libgcc_s.so library (ex: EMFILE error). - return; -} - - -static void -release_sentinel(void *weakref_raw) -{ - PyObject *weakref = _PyObject_CAST(weakref_raw); - - /* Tricky: this function is called when the current thread state - is being deleted. Therefore, only simple C code can safely - execute here. */ - lockobject *lock = (lockobject *)_PyWeakref_GET_REF(weakref); - if (lock != NULL) { - if (lock->locked) { - lock->locked = 0; - PyThread_release_lock(lock->lock_lock); - } - Py_DECREF(lock); - } - - /* Deallocating a weakref with a NULL callback only calls - PyObject_GC_Del(), which can't call any Python code. */ - Py_DECREF(weakref); -} - -static int -set_threadstate_finalizer(PyThreadState *tstate, PyObject *lock) -{ - PyObject *wr; - if (tstate->on_delete_data != NULL) { - /* We must support the re-creation of the lock from a - fork()ed child. */ - assert(tstate->on_delete == &release_sentinel); - wr = (PyObject *) tstate->on_delete_data; - tstate->on_delete = NULL; - tstate->on_delete_data = NULL; - Py_DECREF(wr); - } - - /* The lock is owned by whoever called set_threadstate_finalizer(), - but the weakref clings to the thread state. */ - wr = PyWeakref_NewRef(lock, NULL); - if (wr == NULL) { - return -1; - } - tstate->on_delete_data = (void *) wr; - tstate->on_delete = &release_sentinel; - return 0; -} - - /********************/ /* Module functions */ /********************/ From 23ca97bcc867785d78a1c97239b25722084057ae Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 17:19:43 -0600 Subject: [PATCH 09/14] Clean up the _PyThreadHandle code. --- Modules/_threadmodule.c | 256 ++++++++++++++++++++++++---------------- 1 file changed, 154 insertions(+), 102 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 52e98cc286f6b3..65eb82659ca84a 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -3,7 +3,7 @@ #include "Python.h" #include "pycore_interp.h" // _PyInterpreterState.threads.count -#include "pycore_lock.h" +#include "pycore_lock.h" // _PyEventRc #include "pycore_moduleobject.h" // _PyModule_GetState() #include "pycore_modsupport.h" // _PyArg_NoKeywords() #include "pycore_pylifecycle.h" @@ -47,9 +47,9 @@ get_thread_state(PyObject *module) } -/**********************/ -/* _ThreadHandle type */ -/**********************/ +/*************************/ +/* _ThreadHandle objects */ +/*************************/ // Handles transition from RUNNING to one of JOINED, DETACHED, or INVALID (post // fork). @@ -58,7 +58,12 @@ typedef enum { THREAD_HANDLE_JOINED = 2, THREAD_HANDLE_DETACHED = 3, THREAD_HANDLE_INVALID = 4, -} ThreadHandleState; +} _PyThreadHandleState; + + +// ThreadHandleError is just an alias to PyExc_RuntimeError. +#define ThreadHandleError PyExc_RuntimeError + // A handle around an OS thread. // @@ -78,7 +83,9 @@ typedef struct { PyThread_ident_t ident; PyThread_handle_t handle; - // Holds a value from the `ThreadHandleState` enum. + // Holds the handle's simple state. + // The type is actually _PyThreadHandleState but we use int + // for the _Py_atomic API. int state; // Set immediately before `thread_run` returns to indicate that the OS @@ -89,29 +96,48 @@ typedef struct { // Serializes calls to `join`. _PyOnceFlag once; -} ThreadHandleObject; +} thandleobject; + static inline int -get_thread_handle_state(ThreadHandleObject *handle) +get_thread_handle_state(thandleobject *hobj) { - return _Py_atomic_load_int(&handle->state); + return _Py_atomic_load_int(&hobj->state); } static inline void -set_thread_handle_state(ThreadHandleObject *handle, ThreadHandleState state) +set_thread_handle_state(thandleobject *hobj, _PyThreadHandleState state) { - _Py_atomic_store_int(&handle->state, state); + _Py_atomic_store_int(&hobj->state, state); +} + +static int +join_thread(thandleobject *hobj) +{ + assert(get_thread_handle_state(hobj) == THREAD_HANDLE_RUNNING); + + int err; + Py_BEGIN_ALLOW_THREADS + err = PyThread_join_thread(hobj->handle); + Py_END_ALLOW_THREADS + if (err) { + PyErr_SetString(ThreadHandleError, "Failed joining thread"); + return -1; + } + set_thread_handle_state(hobj, THREAD_HANDLE_JOINED); + return 0; } -static ThreadHandleObject* -new_thread_handle(thread_module_state* state) + +static PyObject * +_PyThreadHandle_NewObject(PyTypeObject *type) { _PyEventRc *event = _PyEventRc_New(); if (event == NULL) { PyErr_NoMemory(); return NULL; } - ThreadHandleObject* self = PyObject_New(ThreadHandleObject, state->thread_handle_type); + thandleobject *self = PyObject_New(thandleobject, type); if (self == NULL) { _PyEventRc_Decref(event); return NULL; @@ -126,95 +152,33 @@ new_thread_handle(thread_module_state* state) llist_insert_tail(&_PyRuntime.threads.handles, &self->node); HEAD_UNLOCK(&_PyRuntime); - return self; + return (PyObject *)self; } -static void -ThreadHandle_dealloc(ThreadHandleObject *self) -{ - PyObject *tp = (PyObject *) Py_TYPE(self); - // Remove ourself from the global list of handles - HEAD_LOCK(&_PyRuntime); - if (self->node.next != NULL) { - llist_remove(&self->node); - } - HEAD_UNLOCK(&_PyRuntime); - - // It's safe to access state non-atomically: - // 1. This is the destructor; nothing else holds a reference. - // 2. The refcount going to zero is a "synchronizes-with" event; - // all changes from other threads are visible. - if (self->state == THREAD_HANDLE_RUNNING) { - // This is typically short so no need to release the GIL - if (PyThread_detach_thread(self->handle)) { - PyErr_SetString(ThreadError, "Failed detaching thread"); - PyErr_WriteUnraisable(tp); - } - else { - self->state = THREAD_HANDLE_DETACHED; - } - } - _PyEventRc_Decref(self->thread_is_exiting); - PyObject_Free(self); - Py_DECREF(tp); -} - -void -_PyThread_AfterFork(struct _pythread_runtime_state *state) +static void +_PyThreadHandle_SetStarted(PyObject *obj, + PyThread_handle_t handle, PyThread_ident_t ident) { - // gh-115035: We mark ThreadHandles as not joinable early in the child's - // after-fork handler. We do this before calling any Python code to ensure - // that it happens before any ThreadHandles are deallocated, such as by a - // GC cycle. - PyThread_ident_t current = PyThread_get_thread_ident_ex(); - - struct llist_node *node; - llist_for_each_safe(node, &state->handles) { - ThreadHandleObject *hobj = llist_data(node, ThreadHandleObject, node); - if (hobj->ident == current) { - continue; - } - - // Disallow calls to join() as they could crash. We are the only - // thread; it's safe to set this without an atomic. - hobj->state = THREAD_HANDLE_INVALID; - llist_remove(node); - } + thandleobject *hobj = (thandleobject *)obj; + assert(get_thread_handle_state(hobj) == THREAD_HANDLE_INVALID); + hobj->handle = handle; + hobj->ident = ident; + set_thread_handle_state(hobj, THREAD_HANDLE_RUNNING); } -static PyObject * -ThreadHandle_repr(ThreadHandleObject *self) -{ - return PyUnicode_FromFormat("<%s object: ident=%" PY_FORMAT_THREAD_IDENT_T ">", - Py_TYPE(self)->tp_name, self->ident); -} -static PyObject * -ThreadHandle_get_ident(ThreadHandleObject *self, void *ignored) +static _PyEventRc * +_PyThreadHandle_GetExitingEvent(PyObject *hobj) { - return PyLong_FromUnsignedLongLong(self->ident); + return ((thandleobject *)hobj)->thread_is_exiting; } -static int -join_thread(ThreadHandleObject *handle) -{ - assert(get_thread_handle_state(handle) == THREAD_HANDLE_RUNNING); - int err; - Py_BEGIN_ALLOW_THREADS - err = PyThread_join_thread(handle->handle); - Py_END_ALLOW_THREADS - if (err) { - PyErr_SetString(ThreadError, "Failed joining thread"); - return -1; - } - set_thread_handle_state(handle, THREAD_HANDLE_JOINED); - return 0; -} +/* _ThreadHandle instance methods */ static PyObject * -ThreadHandle_join(ThreadHandleObject *self, void* ignored) +ThreadHandle_join(thandleobject *self, void* ignored) { if (get_thread_handle_state(self) == THREAD_HANDLE_INVALID) { PyErr_SetString(PyExc_ValueError, @@ -235,7 +199,7 @@ ThreadHandle_join(ThreadHandleObject *self, void* ignored) if (!_PyEvent_IsSet(&self->thread_is_exiting->event) && self->ident == PyThread_get_thread_ident_ex()) { // PyThread_join_thread() would deadlock or error out. - PyErr_SetString(ThreadError, "Cannot join current thread"); + PyErr_SetString(ThreadHandleError, "Cannot join current thread"); return NULL; } @@ -247,10 +211,6 @@ ThreadHandle_join(ThreadHandleObject *self, void* ignored) Py_RETURN_NONE; } -static PyGetSetDef ThreadHandle_getsetlist[] = { - {"ident", (getter)ThreadHandle_get_ident, NULL, NULL}, - {0}, -}; static PyMethodDef ThreadHandle_methods[] = { @@ -258,6 +218,63 @@ static PyMethodDef ThreadHandle_methods[] = {0, 0} }; + +/* _ThreadHandle instance properties */ + +static PyObject * +ThreadHandle_get_ident(thandleobject *self, void *ignored) +{ + return PyLong_FromUnsignedLongLong(self->ident); +} + + +static PyGetSetDef ThreadHandle_getsetlist[] = { + {"ident", (getter)ThreadHandle_get_ident, NULL, NULL}, + {0}, +}; + + +/* The _ThreadHandle class */ + +static void +ThreadHandle_dealloc(thandleobject *self) +{ + PyObject *tp = (PyObject *) Py_TYPE(self); + + // Remove ourself from the global list of handles + HEAD_LOCK(&_PyRuntime); + if (self->node.next != NULL) { + llist_remove(&self->node); + } + HEAD_UNLOCK(&_PyRuntime); + + // It's safe to access state non-atomically: + // 1. This is the destructor; nothing else holds a reference. + // 2. The refcount going to zero is a "synchronizes-with" event; + // all changes from other threads are visible. + if (self->state == THREAD_HANDLE_RUNNING) { + // This is typically short so no need to release the GIL + if (PyThread_detach_thread(self->handle)) { + PyErr_SetString(ThreadHandleError, "Failed detaching thread"); + PyErr_WriteUnraisable(tp); + } + else { + self->state = THREAD_HANDLE_DETACHED; + } + } + _PyEventRc_Decref(self->thread_is_exiting); + PyObject_Free(self); + Py_DECREF(tp); +} + +static PyObject * +ThreadHandle_repr(thandleobject *self) +{ + return PyUnicode_FromFormat("<%s object: ident=%" PY_FORMAT_THREAD_IDENT_T ">", + Py_TYPE(self)->tp_name, self->ident); +} + + static PyType_Slot ThreadHandle_Type_slots[] = { {Py_tp_dealloc, (destructor)ThreadHandle_dealloc}, {Py_tp_repr, (reprfunc)ThreadHandle_repr}, @@ -268,13 +285,46 @@ static PyType_Slot ThreadHandle_Type_slots[] = { static PyType_Spec ThreadHandle_Type_spec = { "_thread._ThreadHandle", - sizeof(ThreadHandleObject), + sizeof(thandleobject), 0, Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, ThreadHandle_Type_slots, }; +PyTypeObject * +_PyThreadHandle_NewType(void) +{ + return (PyTypeObject *)PyType_FromSpec(&ThreadHandle_Type_spec); +} + + +/* other API */ + +void +_PyThread_AfterFork(struct _pythread_runtime_state *state) +{ + // gh-115035: We mark ThreadHandles as not joinable early in the child's + // after-fork handler. We do this before calling any Python code to ensure + // that it happens before any ThreadHandles are deallocated, such as by a + // GC cycle. + PyThread_ident_t current = PyThread_get_thread_ident_ex(); + + struct llist_node *node; + llist_for_each_safe(node, &state->handles) { + thandleobject *hobj = llist_data(node, thandleobject, node); + if (hobj->ident == current) { + continue; + } + + // Disallow calls to join() as they could crash. We are the only + // thread; it's safe to set this without an atomic. + hobj->state = THREAD_HANDLE_INVALID; + llist_remove(node); + } +} + + /********************/ /* thread execution */ /********************/ @@ -1785,26 +1835,28 @@ threadmod_start_joinable_thread(PyObject *module, PyObject *func) return NULL; } - ThreadHandleObject* hobj = new_thread_handle(state); + PyObject *hobj = _PyThreadHandle_NewObject(state->thread_handle_type); if (hobj == NULL) { return NULL; } struct bootstate *boot = thread_bootstate_new( - func, NULL, NULL, hobj->thread_is_exiting); + func, NULL, NULL, _PyThreadHandle_GetExitingEvent(hobj)); if (boot == NULL) { Py_DECREF(hobj); return NULL; } + PyThread_ident_t ident = 0; + PyThread_handle_t handle = 0; if (PyThread_start_joinable_thread( - thread_run, (void*) boot, &hobj->ident, &hobj->handle) < 0) + thread_run, (void*) boot, &ident, &handle) < 0) { PyErr_SetString(ThreadError, "can't start new thread"); thread_bootstate_free(boot); Py_DECREF(hobj); return NULL; } - set_thread_handle_state(hobj, THREAD_HANDLE_RUNNING); - return (PyObject*) hobj; + _PyThreadHandle_SetStarted(hobj, handle, ident); + return hobj; } PyDoc_STRVAR(start_joinable_doc, @@ -2055,7 +2107,7 @@ thread_module_exec(PyObject *module) PyThread_init_thread(); // _ThreadHandle - state->thread_handle_type = (PyTypeObject *)PyType_FromSpec(&ThreadHandle_Type_spec); + state->thread_handle_type = _PyThreadHandle_NewType(); if (state->thread_handle_type == NULL) { return -1; } From 41c7bd8c65f5fe6743637eff12bb85b712bc2541 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 19:00:47 -0600 Subject: [PATCH 10/14] Factor out functions for tracking thread handles for fork. --- Modules/_threadmodule.c | 67 +++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 65eb82659ca84a..4cbd844b9cd534 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -75,7 +75,7 @@ typedef enum { // join has completed successfully all future joins complete immediately. typedef struct { PyObject_HEAD - struct llist_node node; // linked list node (see _pythread_runtime_state) + struct llist_node fork_node; // linked list node (see _pythread_runtime_state) // The `ident` and `handle` fields are immutable once the object is visible // to threads other than its creator, thus they do not need to be accessed @@ -129,6 +129,8 @@ join_thread(thandleobject *hobj) } +static void track_thread_handle_for_fork(thandleobject *); + static PyObject * _PyThreadHandle_NewObject(PyTypeObject *type) { @@ -148,9 +150,7 @@ _PyThreadHandle_NewObject(PyTypeObject *type) self->once = (_PyOnceFlag){0}; self->state = THREAD_HANDLE_INVALID; - HEAD_LOCK(&_PyRuntime); - llist_insert_tail(&_PyRuntime.threads.handles, &self->node); - HEAD_UNLOCK(&_PyRuntime); + track_thread_handle_for_fork(self); return (PyObject *)self; } @@ -175,6 +175,45 @@ _PyThreadHandle_GetExitingEvent(PyObject *hobj) } +/* tracking thread handles for fork */ + +static void +track_thread_handle_for_fork(thandleobject *hobj) +{ + HEAD_LOCK(&_PyRuntime); + llist_insert_tail(&_PyRuntime.threads.handles, &hobj->fork_node); + HEAD_UNLOCK(&_PyRuntime); +} + +static void +untrack_thread_handle_for_fork(thandleobject *hobj) +{ + HEAD_LOCK(&_PyRuntime); + if (hobj->fork_node.next != NULL) { + llist_remove(&hobj->fork_node); + } + HEAD_UNLOCK(&_PyRuntime); +} + +static void +clear_tracked_thread_handles(struct _pythread_runtime_state *state, + PyThread_ident_t current) +{ + struct llist_node *node; + llist_for_each_safe(node, &state->handles) { + thandleobject *hobj = llist_data(node, thandleobject, fork_node); + if (hobj->ident == current) { + continue; + } + + // Disallow calls to join() as they could crash. We are the only + // thread; it's safe to set this without an atomic. + hobj->state = THREAD_HANDLE_INVALID; + llist_remove(node); + } +} + + /* _ThreadHandle instance methods */ static PyObject * @@ -242,11 +281,7 @@ ThreadHandle_dealloc(thandleobject *self) PyObject *tp = (PyObject *) Py_TYPE(self); // Remove ourself from the global list of handles - HEAD_LOCK(&_PyRuntime); - if (self->node.next != NULL) { - llist_remove(&self->node); - } - HEAD_UNLOCK(&_PyRuntime); + untrack_thread_handle_for_fork(self); // It's safe to access state non-atomically: // 1. This is the destructor; nothing else holds a reference. @@ -309,19 +344,7 @@ _PyThread_AfterFork(struct _pythread_runtime_state *state) // that it happens before any ThreadHandles are deallocated, such as by a // GC cycle. PyThread_ident_t current = PyThread_get_thread_ident_ex(); - - struct llist_node *node; - llist_for_each_safe(node, &state->handles) { - thandleobject *hobj = llist_data(node, thandleobject, node); - if (hobj->ident == current) { - continue; - } - - // Disallow calls to join() as they could crash. We are the only - // thread; it's safe to set this without an atomic. - hobj->state = THREAD_HANDLE_INVALID; - llist_remove(node); - } + clear_tracked_thread_handles(state, current); } From 3db60f2daaaf9d8a60d37a3a76f25c98f744aa7d Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 10:52:47 -0600 Subject: [PATCH 11/14] Factor out threadhandleobject.c. --- Include/internal/pycore_pythread.h | 26 +++ Makefile.pre.in | 1 + Modules/_threadmodule.c | 301 --------------------------- Objects/threadhandleobject.c | 313 +++++++++++++++++++++++++++++ PCbuild/pythoncore.vcxproj | 1 + PCbuild/pythoncore.vcxproj.filters | 3 + 6 files changed, 344 insertions(+), 301 deletions(-) create mode 100644 Objects/threadhandleobject.c diff --git a/Include/internal/pycore_pythread.h b/Include/internal/pycore_pythread.h index d2e7cc2a206ced..34ffcb67742f1f 100644 --- a/Include/internal/pycore_pythread.h +++ b/Include/internal/pycore_pythread.h @@ -153,6 +153,32 @@ PyAPI_FUNC(int) PyThread_join_thread(PyThread_handle_t); */ PyAPI_FUNC(int) PyThread_detach_thread(PyThread_handle_t); + +/******************/ +/* thread handles */ +/******************/ + +// Handles transition from RUNNING to one of JOINED, DETACHED, or INVALID (post +// fork). +typedef enum { + THREAD_HANDLE_RUNNING = 1, + THREAD_HANDLE_JOINED = 2, + THREAD_HANDLE_DETACHED = 3, + THREAD_HANDLE_INVALID = 4, +} _PyThreadHandleState; + +// XXX Make it a static type. +extern PyTypeObject * _PyThreadHandle_NewType(void); + +extern PyObject * _PyThreadHandle_NewObject(PyTypeObject *); +extern _PyEventRc * _PyThreadHandle_GetExitingEvent(PyObject *); +extern void _PyThreadHandle_SetStarted( + PyObject *obj, + PyThread_handle_t handle, + PyThread_ident_t ident +); + + #ifdef __cplusplus } #endif diff --git a/Makefile.pre.in b/Makefile.pre.in index 3cf4de08a0c842..fa436c47135742 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -524,6 +524,7 @@ OBJECT_OBJS= \ Objects/setobject.o \ Objects/sliceobject.o \ Objects/structseq.o \ + Objects/threadhandleobject.o \ Objects/tupleobject.o \ Objects/typeobject.o \ Objects/typevarobject.o \ diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 4cbd844b9cd534..1692d6b1f49fbe 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -47,307 +47,6 @@ get_thread_state(PyObject *module) } -/*************************/ -/* _ThreadHandle objects */ -/*************************/ - -// Handles transition from RUNNING to one of JOINED, DETACHED, or INVALID (post -// fork). -typedef enum { - THREAD_HANDLE_RUNNING = 1, - THREAD_HANDLE_JOINED = 2, - THREAD_HANDLE_DETACHED = 3, - THREAD_HANDLE_INVALID = 4, -} _PyThreadHandleState; - - -// ThreadHandleError is just an alias to PyExc_RuntimeError. -#define ThreadHandleError PyExc_RuntimeError - - -// A handle around an OS thread. -// -// The OS thread is either joined or detached after the handle is destroyed. -// -// Joining the handle is idempotent; the underlying OS thread is joined or -// detached only once. Concurrent join operations are serialized until it is -// their turn to execute or an earlier operation completes successfully. Once a -// join has completed successfully all future joins complete immediately. -typedef struct { - PyObject_HEAD - struct llist_node fork_node; // linked list node (see _pythread_runtime_state) - - // The `ident` and `handle` fields are immutable once the object is visible - // to threads other than its creator, thus they do not need to be accessed - // atomically. - PyThread_ident_t ident; - PyThread_handle_t handle; - - // Holds the handle's simple state. - // The type is actually _PyThreadHandleState but we use int - // for the _Py_atomic API. - int state; - - // Set immediately before `thread_run` returns to indicate that the OS - // thread is about to exit. This is used to avoid false positives when - // detecting self-join attempts. See the comment in `ThreadHandle_join()` - // for a more detailed explanation. - _PyEventRc *thread_is_exiting; - - // Serializes calls to `join`. - _PyOnceFlag once; -} thandleobject; - - -static inline int -get_thread_handle_state(thandleobject *hobj) -{ - return _Py_atomic_load_int(&hobj->state); -} - -static inline void -set_thread_handle_state(thandleobject *hobj, _PyThreadHandleState state) -{ - _Py_atomic_store_int(&hobj->state, state); -} - -static int -join_thread(thandleobject *hobj) -{ - assert(get_thread_handle_state(hobj) == THREAD_HANDLE_RUNNING); - - int err; - Py_BEGIN_ALLOW_THREADS - err = PyThread_join_thread(hobj->handle); - Py_END_ALLOW_THREADS - if (err) { - PyErr_SetString(ThreadHandleError, "Failed joining thread"); - return -1; - } - set_thread_handle_state(hobj, THREAD_HANDLE_JOINED); - return 0; -} - - -static void track_thread_handle_for_fork(thandleobject *); - -static PyObject * -_PyThreadHandle_NewObject(PyTypeObject *type) -{ - _PyEventRc *event = _PyEventRc_New(); - if (event == NULL) { - PyErr_NoMemory(); - return NULL; - } - thandleobject *self = PyObject_New(thandleobject, type); - if (self == NULL) { - _PyEventRc_Decref(event); - return NULL; - } - self->ident = 0; - self->handle = 0; - self->thread_is_exiting = event; - self->once = (_PyOnceFlag){0}; - self->state = THREAD_HANDLE_INVALID; - - track_thread_handle_for_fork(self); - - return (PyObject *)self; -} - - -static void -_PyThreadHandle_SetStarted(PyObject *obj, - PyThread_handle_t handle, PyThread_ident_t ident) -{ - thandleobject *hobj = (thandleobject *)obj; - assert(get_thread_handle_state(hobj) == THREAD_HANDLE_INVALID); - hobj->handle = handle; - hobj->ident = ident; - set_thread_handle_state(hobj, THREAD_HANDLE_RUNNING); -} - - -static _PyEventRc * -_PyThreadHandle_GetExitingEvent(PyObject *hobj) -{ - return ((thandleobject *)hobj)->thread_is_exiting; -} - - -/* tracking thread handles for fork */ - -static void -track_thread_handle_for_fork(thandleobject *hobj) -{ - HEAD_LOCK(&_PyRuntime); - llist_insert_tail(&_PyRuntime.threads.handles, &hobj->fork_node); - HEAD_UNLOCK(&_PyRuntime); -} - -static void -untrack_thread_handle_for_fork(thandleobject *hobj) -{ - HEAD_LOCK(&_PyRuntime); - if (hobj->fork_node.next != NULL) { - llist_remove(&hobj->fork_node); - } - HEAD_UNLOCK(&_PyRuntime); -} - -static void -clear_tracked_thread_handles(struct _pythread_runtime_state *state, - PyThread_ident_t current) -{ - struct llist_node *node; - llist_for_each_safe(node, &state->handles) { - thandleobject *hobj = llist_data(node, thandleobject, fork_node); - if (hobj->ident == current) { - continue; - } - - // Disallow calls to join() as they could crash. We are the only - // thread; it's safe to set this without an atomic. - hobj->state = THREAD_HANDLE_INVALID; - llist_remove(node); - } -} - - -/* _ThreadHandle instance methods */ - -static PyObject * -ThreadHandle_join(thandleobject *self, void* ignored) -{ - if (get_thread_handle_state(self) == THREAD_HANDLE_INVALID) { - PyErr_SetString(PyExc_ValueError, - "the handle is invalid and thus cannot be joined"); - return NULL; - } - - // We want to perform this check outside of the `_PyOnceFlag` to prevent - // deadlock in the scenario where another thread joins us and we then - // attempt to join ourselves. However, it's not safe to check thread - // identity once the handle's os thread has finished. We may end up reusing - // the identity stored in the handle and erroneously think we are - // attempting to join ourselves. - // - // 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->event) && - self->ident == PyThread_get_thread_ident_ex()) { - // PyThread_join_thread() would deadlock or error out. - PyErr_SetString(ThreadHandleError, "Cannot join current thread"); - return NULL; - } - - if (_PyOnceFlag_CallOnce(&self->once, (_Py_once_fn_t *)join_thread, - self) == -1) { - return NULL; - } - assert(get_thread_handle_state(self) == THREAD_HANDLE_JOINED); - Py_RETURN_NONE; -} - - -static PyMethodDef ThreadHandle_methods[] = -{ - {"join", (PyCFunction)ThreadHandle_join, METH_NOARGS}, - {0, 0} -}; - - -/* _ThreadHandle instance properties */ - -static PyObject * -ThreadHandle_get_ident(thandleobject *self, void *ignored) -{ - return PyLong_FromUnsignedLongLong(self->ident); -} - - -static PyGetSetDef ThreadHandle_getsetlist[] = { - {"ident", (getter)ThreadHandle_get_ident, NULL, NULL}, - {0}, -}; - - -/* The _ThreadHandle class */ - -static void -ThreadHandle_dealloc(thandleobject *self) -{ - PyObject *tp = (PyObject *) Py_TYPE(self); - - // Remove ourself from the global list of handles - untrack_thread_handle_for_fork(self); - - // It's safe to access state non-atomically: - // 1. This is the destructor; nothing else holds a reference. - // 2. The refcount going to zero is a "synchronizes-with" event; - // all changes from other threads are visible. - if (self->state == THREAD_HANDLE_RUNNING) { - // This is typically short so no need to release the GIL - if (PyThread_detach_thread(self->handle)) { - PyErr_SetString(ThreadHandleError, "Failed detaching thread"); - PyErr_WriteUnraisable(tp); - } - else { - self->state = THREAD_HANDLE_DETACHED; - } - } - _PyEventRc_Decref(self->thread_is_exiting); - PyObject_Free(self); - Py_DECREF(tp); -} - -static PyObject * -ThreadHandle_repr(thandleobject *self) -{ - return PyUnicode_FromFormat("<%s object: ident=%" PY_FORMAT_THREAD_IDENT_T ">", - Py_TYPE(self)->tp_name, self->ident); -} - - -static PyType_Slot ThreadHandle_Type_slots[] = { - {Py_tp_dealloc, (destructor)ThreadHandle_dealloc}, - {Py_tp_repr, (reprfunc)ThreadHandle_repr}, - {Py_tp_getset, ThreadHandle_getsetlist}, - {Py_tp_methods, ThreadHandle_methods}, - {0, 0} -}; - -static PyType_Spec ThreadHandle_Type_spec = { - "_thread._ThreadHandle", - sizeof(thandleobject), - 0, - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, - ThreadHandle_Type_slots, -}; - - -PyTypeObject * -_PyThreadHandle_NewType(void) -{ - return (PyTypeObject *)PyType_FromSpec(&ThreadHandle_Type_spec); -} - - -/* other API */ - -void -_PyThread_AfterFork(struct _pythread_runtime_state *state) -{ - // gh-115035: We mark ThreadHandles as not joinable early in the child's - // after-fork handler. We do this before calling any Python code to ensure - // that it happens before any ThreadHandles are deallocated, such as by a - // GC cycle. - PyThread_ident_t current = PyThread_get_thread_ident_ex(); - clear_tracked_thread_handles(state, current); -} - - /********************/ /* thread execution */ /********************/ diff --git a/Objects/threadhandleobject.c b/Objects/threadhandleobject.c new file mode 100644 index 00000000000000..34d5851d6ad491 --- /dev/null +++ b/Objects/threadhandleobject.c @@ -0,0 +1,313 @@ +/* ThreadHandle object (see Modules/_threadmodule.c) */ + +#include "Python.h" +#include "pycore_lock.h" // _PyEventRc +#include "pycore_pystate.h" // HEAD_LOCK() +#include "pycore_pythread.h" // ThreadHandleState +#include "pycore_runtime.h" // _PyRuntime + + +// ThreadHandleError is just an alias to PyExc_RuntimeError. +#define ThreadHandleError PyExc_RuntimeError + + +// A handle around an OS thread. +// +// The OS thread is either joined or detached after the handle is destroyed. +// +// Joining the handle is idempotent; the underlying OS thread is joined or +// detached only once. Concurrent join operations are serialized until it is +// their turn to execute or an earlier operation completes successfully. Once a +// join has completed successfully all future joins complete immediately. +typedef struct { + PyObject_HEAD + struct llist_node fork_node; // linked list node (see _pythread_runtime_state) + + // The `ident` and `handle` fields are immutable once the object is visible + // to threads other than its creator, thus they do not need to be accessed + // atomically. + PyThread_ident_t ident; + PyThread_handle_t handle; + + // Holds the handle's simple state. + // The type is actually _PyThreadHandleState but we use int + // for the _Py_atomic API. + int state; + + // Set immediately before `thread_run` returns to indicate that the OS + // thread is about to exit. This is used to avoid false positives when + // detecting self-join attempts. See the comment in `ThreadHandle_join()` + // for a more detailed explanation. + _PyEventRc *thread_is_exiting; + + // Serializes calls to `join`. + _PyOnceFlag once; +} thandleobject; + + +/***************************/ +/* internal implementation */ +/***************************/ + +static inline int +get_thread_handle_state(thandleobject *hobj) +{ + return _Py_atomic_load_int(&hobj->state); +} + +static inline void +set_thread_handle_state(thandleobject *hobj, _PyThreadHandleState state) +{ + _Py_atomic_store_int(&hobj->state, state); +} + +static int +join_thread(thandleobject *hobj) +{ + assert(get_thread_handle_state(hobj) == THREAD_HANDLE_RUNNING); + + int err; + Py_BEGIN_ALLOW_THREADS + err = PyThread_join_thread(hobj->handle); + Py_END_ALLOW_THREADS + if (err) { + PyErr_SetString(ThreadHandleError, "Failed joining thread"); + return -1; + } + set_thread_handle_state(hobj, THREAD_HANDLE_JOINED); + return 0; +} + + +/****************************************/ +/* _ThreadHandle object C-API functions */ +/****************************************/ + +static void track_thread_handle_for_fork(thandleobject *); + +PyObject * +_PyThreadHandle_NewObject(PyTypeObject *type) +{ + _PyEventRc *event = _PyEventRc_New(); + if (event == NULL) { + PyErr_NoMemory(); + return NULL; + } + thandleobject *self = PyObject_New(thandleobject, type); + if (self == NULL) { + _PyEventRc_Decref(event); + return NULL; + } + self->ident = 0; + self->handle = 0; + self->thread_is_exiting = event; + self->once = (_PyOnceFlag){0}; + self->state = THREAD_HANDLE_INVALID; + + track_thread_handle_for_fork(self); + + return (PyObject *)self; +} + + +void +_PyThreadHandle_SetStarted(PyObject *obj, + PyThread_handle_t handle, PyThread_ident_t ident) +{ + thandleobject *hobj = (thandleobject *)obj; + assert(get_thread_handle_state(hobj) == THREAD_HANDLE_INVALID); + hobj->handle = handle; + hobj->ident = ident; + set_thread_handle_state(hobj, THREAD_HANDLE_RUNNING); +} + + +_PyEventRc * +_PyThreadHandle_GetExitingEvent(PyObject *hobj) +{ + return ((thandleobject *)hobj)->thread_is_exiting; +} + + +/************************************/ +/* tracking thread handles for fork */ +/************************************/ + +static void +track_thread_handle_for_fork(thandleobject *hobj) +{ + HEAD_LOCK(&_PyRuntime); + llist_insert_tail(&_PyRuntime.threads.handles, &hobj->fork_node); + HEAD_UNLOCK(&_PyRuntime); +} + +static void +untrack_thread_handle_for_fork(thandleobject *hobj) +{ + HEAD_LOCK(&_PyRuntime); + if (hobj->fork_node.next != NULL) { + llist_remove(&hobj->fork_node); + } + HEAD_UNLOCK(&_PyRuntime); +} + +static void +clear_tracked_thread_handles(struct _pythread_runtime_state *state, + PyThread_ident_t current) +{ + struct llist_node *node; + llist_for_each_safe(node, &state->handles) { + thandleobject *hobj = llist_data(node, thandleobject, fork_node); + if (hobj->ident == current) { + continue; + } + + // Disallow calls to join() as they could crash. We are the only + // thread; it's safe to set this without an atomic. + hobj->state = THREAD_HANDLE_INVALID; + llist_remove(node); + } +} + + +/**********************************/ +/* _ThreadHandle instance methods */ +/**********************************/ + +static PyObject * +ThreadHandle_join(thandleobject *self, void* ignored) +{ + if (get_thread_handle_state(self) == THREAD_HANDLE_INVALID) { + PyErr_SetString(PyExc_ValueError, + "the handle is invalid and thus cannot be joined"); + return NULL; + } + + // We want to perform this check outside of the `_PyOnceFlag` to prevent + // deadlock in the scenario where another thread joins us and we then + // attempt to join ourselves. However, it's not safe to check thread + // identity once the handle's os thread has finished. We may end up reusing + // the identity stored in the handle and erroneously think we are + // attempting to join ourselves. + // + // 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->event) && + self->ident == PyThread_get_thread_ident_ex()) { + // PyThread_join_thread() would deadlock or error out. + PyErr_SetString(ThreadHandleError, "Cannot join current thread"); + return NULL; + } + + if (_PyOnceFlag_CallOnce(&self->once, (_Py_once_fn_t *)join_thread, + self) == -1) { + return NULL; + } + assert(get_thread_handle_state(self) == THREAD_HANDLE_JOINED); + Py_RETURN_NONE; +} + + +static PyMethodDef ThreadHandle_methods[] = +{ + {"join", (PyCFunction)ThreadHandle_join, METH_NOARGS}, + {0, 0} +}; + + +/*************************************/ +/* _ThreadHandle instance properties */ +/*************************************/ + +static PyObject * +ThreadHandle_get_ident(thandleobject *self, void *ignored) +{ + return PyLong_FromUnsignedLongLong(self->ident); +} + + +static PyGetSetDef ThreadHandle_getsetlist[] = { + {"ident", (getter)ThreadHandle_get_ident, NULL, NULL}, + {0}, +}; + + +/***************************/ +/* The _ThreadHandle class */ +/***************************/ + +static void +ThreadHandle_dealloc(thandleobject *self) +{ + PyObject *tp = (PyObject *) Py_TYPE(self); + + // Remove ourself from the global list of handles + untrack_thread_handle_for_fork(self); + + // It's safe to access state non-atomically: + // 1. This is the destructor; nothing else holds a reference. + // 2. The refcount going to zero is a "synchronizes-with" event; + // all changes from other threads are visible. + if (self->state == THREAD_HANDLE_RUNNING) { + // This is typically short so no need to release the GIL + if (PyThread_detach_thread(self->handle)) { + PyErr_SetString(ThreadHandleError, "Failed detaching thread"); + PyErr_WriteUnraisable(tp); + } + else { + set_thread_handle_state(self, THREAD_HANDLE_DETACHED); + } + } + _PyEventRc_Decref(self->thread_is_exiting); + PyObject_Free(self); + Py_DECREF(tp); +} + + +static PyObject * +ThreadHandle_repr(thandleobject *self) +{ + return PyUnicode_FromFormat("<%s object: ident=%" PY_FORMAT_THREAD_IDENT_T ">", + Py_TYPE(self)->tp_name, self->ident); +} + + +static PyType_Slot ThreadHandle_Type_slots[] = { + {Py_tp_dealloc, (destructor)ThreadHandle_dealloc}, + {Py_tp_repr, (reprfunc)ThreadHandle_repr}, + {Py_tp_getset, ThreadHandle_getsetlist}, + {Py_tp_methods, ThreadHandle_methods}, + {0, 0} +}; + +static PyType_Spec ThreadHandle_Type_spec = { + "_thread._ThreadHandle", + sizeof(thandleobject), + 0, + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, + ThreadHandle_Type_slots, +}; + + +PyTypeObject * +_PyThreadHandle_NewType(void) +{ + return (PyTypeObject *)PyType_FromSpec(&ThreadHandle_Type_spec); +} + + +/*************/ +/* other API */ +/*************/ + +void +_PyThread_AfterFork(struct _pythread_runtime_state *state) +{ + // gh-115035: We mark ThreadHandles as not joinable early in the child's + // after-fork handler. We do this before calling any Python code to ensure + // that it happens before any ThreadHandles are deallocated, such as by a + // GC cycle. + PyThread_ident_t current = PyThread_get_thread_ident_ex(); + clear_tracked_thread_handles(state, current); +} diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index 88a4a7c9564309..e7b7f487eef768 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -520,6 +520,7 @@ + diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index 27bd1121663398..9c51373331da8c 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -1184,6 +1184,9 @@ Objects + + Objects + Objects From d2fb9bdc758f39c165a94571843530dfa1ca7c68 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 15:09:13 -0600 Subject: [PATCH 12/14] Make _ThreadHandle a static builtin type. --- Include/internal/pycore_pythread.h | 5 ++--- Modules/_threadmodule.c | 15 ++++--------- Objects/object.c | 1 + Objects/threadhandleobject.c | 35 ++++++++++-------------------- 4 files changed, 18 insertions(+), 38 deletions(-) diff --git a/Include/internal/pycore_pythread.h b/Include/internal/pycore_pythread.h index 34ffcb67742f1f..39c666ac026132 100644 --- a/Include/internal/pycore_pythread.h +++ b/Include/internal/pycore_pythread.h @@ -167,10 +167,9 @@ typedef enum { THREAD_HANDLE_INVALID = 4, } _PyThreadHandleState; -// XXX Make it a static type. -extern PyTypeObject * _PyThreadHandle_NewType(void); +extern PyTypeObject _PyThreadHandle_Type; -extern PyObject * _PyThreadHandle_NewObject(PyTypeObject *); +extern PyObject * _PyThreadHandle_NewObject(void); extern _PyEventRc * _PyThreadHandle_GetExitingEvent(PyObject *); extern void _PyThreadHandle_SetStarted( PyObject *obj, diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 1692d6b1f49fbe..1072cf649269d4 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -35,7 +35,6 @@ typedef struct { PyTypeObject *lock_type; PyTypeObject *local_type; PyTypeObject *local_dummy_type; - PyTypeObject *thread_handle_type; } thread_module_state; static inline thread_module_state* @@ -1545,8 +1544,6 @@ is SystemExit.\n"); static PyObject * threadmod_start_joinable_thread(PyObject *module, PyObject *func) { - thread_module_state *state = get_thread_state(module); - if (!PyCallable_Check(func)) { PyErr_SetString(PyExc_TypeError, "thread function must be callable"); @@ -1557,7 +1554,7 @@ threadmod_start_joinable_thread(PyObject *module, PyObject *func) return NULL; } - PyObject *hobj = _PyThreadHandle_NewObject(state->thread_handle_type); + PyObject *hobj = _PyThreadHandle_NewObject(); if (hobj == NULL) { return NULL; } @@ -1829,11 +1826,9 @@ thread_module_exec(PyObject *module) PyThread_init_thread(); // _ThreadHandle - state->thread_handle_type = _PyThreadHandle_NewType(); - if (state->thread_handle_type == NULL) { - return -1; - } - if (PyDict_SetItemString(d, "_ThreadHandle", (PyObject *)state->thread_handle_type) < 0) { + if (PyDict_SetItemString(d, "_ThreadHandle", + (PyObject *)&_PyThreadHandle_Type) < 0) + { return -1; } @@ -1914,7 +1909,6 @@ thread_module_traverse(PyObject *module, visitproc visit, void *arg) Py_VISIT(state->lock_type); Py_VISIT(state->local_type); Py_VISIT(state->local_dummy_type); - Py_VISIT(state->thread_handle_type); return 0; } @@ -1926,7 +1920,6 @@ thread_module_clear(PyObject *module) Py_CLEAR(state->lock_type); Py_CLEAR(state->local_type); Py_CLEAR(state->local_dummy_type); - Py_CLEAR(state->thread_handle_type); return 0; } diff --git a/Objects/object.c b/Objects/object.c index df14fe0c6fbfec..d659c24dc987f4 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -2301,6 +2301,7 @@ static PyTypeObject* static_types[] = { &_PyNone_Type, &_PyNotImplemented_Type, &_PyPositionsIterator, + &_PyThreadHandle_Type, &_PyUnicodeASCIIIter_Type, &_PyUnion_Type, &_PyUOpExecutor_Type, diff --git a/Objects/threadhandleobject.c b/Objects/threadhandleobject.c index 34d5851d6ad491..a01194af3976eb 100644 --- a/Objects/threadhandleobject.c +++ b/Objects/threadhandleobject.c @@ -86,14 +86,14 @@ join_thread(thandleobject *hobj) static void track_thread_handle_for_fork(thandleobject *); PyObject * -_PyThreadHandle_NewObject(PyTypeObject *type) +_PyThreadHandle_NewObject(void) { _PyEventRc *event = _PyEventRc_New(); if (event == NULL) { PyErr_NoMemory(); return NULL; } - thandleobject *self = PyObject_New(thandleobject, type); + thandleobject *self = PyObject_New(thandleobject, &_PyThreadHandle_Type); if (self == NULL) { _PyEventRc_Decref(event); return NULL; @@ -261,7 +261,6 @@ ThreadHandle_dealloc(thandleobject *self) } _PyEventRc_Decref(self->thread_is_exiting); PyObject_Free(self); - Py_DECREF(tp); } @@ -273,30 +272,18 @@ ThreadHandle_repr(thandleobject *self) } -static PyType_Slot ThreadHandle_Type_slots[] = { - {Py_tp_dealloc, (destructor)ThreadHandle_dealloc}, - {Py_tp_repr, (reprfunc)ThreadHandle_repr}, - {Py_tp_getset, ThreadHandle_getsetlist}, - {Py_tp_methods, ThreadHandle_methods}, - {0, 0} -}; - -static PyType_Spec ThreadHandle_Type_spec = { - "_thread._ThreadHandle", - sizeof(thandleobject), - 0, - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, - ThreadHandle_Type_slots, +PyTypeObject _PyThreadHandle_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + .tp_name = "_ThreadHandle", + .tp_basicsize = sizeof(thandleobject), + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, + .tp_dealloc = (destructor)ThreadHandle_dealloc, + .tp_repr = (reprfunc)ThreadHandle_repr, + .tp_getset = ThreadHandle_getsetlist, + .tp_methods = ThreadHandle_methods, }; -PyTypeObject * -_PyThreadHandle_NewType(void) -{ - return (PyTypeObject *)PyType_FromSpec(&ThreadHandle_Type_spec); -} - - /*************/ /* other API */ /*************/ From 4d0edee56baf9a9050bc0b0e6acdf18abf2597b4 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 18:23:19 -0600 Subject: [PATCH 13/14] Factor out struct _PyThread_handle_data. --- Objects/threadhandleobject.c | 325 +++++++++++++++++++++-------------- 1 file changed, 194 insertions(+), 131 deletions(-) diff --git a/Objects/threadhandleobject.c b/Objects/threadhandleobject.c index a01194af3976eb..e0d6f6770c96f5 100644 --- a/Objects/threadhandleobject.c +++ b/Objects/threadhandleobject.c @@ -11,6 +11,10 @@ #define ThreadHandleError PyExc_RuntimeError +/**********************/ +/* thread handle data */ +/**********************/ + // A handle around an OS thread. // // The OS thread is either joined or detached after the handle is destroyed. @@ -19,19 +23,14 @@ // detached only once. Concurrent join operations are serialized until it is // their turn to execute or an earlier operation completes successfully. Once a // join has completed successfully all future joins complete immediately. -typedef struct { - PyObject_HEAD - struct llist_node fork_node; // linked list node (see _pythread_runtime_state) - +struct _PyThread_handle_data { // The `ident` and `handle` fields are immutable once the object is visible // to threads other than its creator, thus they do not need to be accessed // atomically. PyThread_ident_t ident; PyThread_handle_t handle; - // Holds the handle's simple state. - // The type is actually _PyThreadHandleState but we use int - // for the _Py_atomic API. + // Holds a value from the `ThreadHandleState` enum. int state; // Set immediately before `thread_run` returns to indicate that the OS @@ -42,7 +41,51 @@ typedef struct { // Serializes calls to `join`. _PyOnceFlag once; -} thandleobject; +}; + + +static struct _PyThread_handle_data * +new_thread_handle_data(void) +{ + _PyEventRc *event = _PyEventRc_New(); + if (event == NULL) { + PyErr_NoMemory(); + return NULL; + } + struct _PyThread_handle_data *data = \ + PyMem_RawMalloc(sizeof(struct _PyThread_handle_data)); + if (data == NULL) { + _PyEventRc_Decref(event); + return NULL; + } + *data = (struct _PyThread_handle_data){ + .thread_is_exiting = event, + .state = THREAD_HANDLE_INVALID, + }; + // XXX Add _PyRuntime.threads.handles here. + return data; +} + +static inline int get_thread_handle_state(struct _PyThread_handle_data *); +static inline void set_thread_handle_state( + struct _PyThread_handle_data *, _PyThreadHandleState); + +static void +free_thread_handle_data(struct _PyThread_handle_data *data) +{ + if (get_thread_handle_state(data) == THREAD_HANDLE_RUNNING) { + // This is typically short so no need to release the GIL + if (PyThread_detach_thread(data->handle)) { + PyErr_SetString(ThreadHandleError, "Failed detaching thread"); + PyErr_WriteUnraisable(NULL); + } + else { + set_thread_handle_state(data, THREAD_HANDLE_DETACHED); + } + } + _PyEventRc_Decref(data->thread_is_exiting); + PyMem_RawFree(data); +} /***************************/ @@ -50,161 +93,135 @@ typedef struct { /***************************/ static inline int -get_thread_handle_state(thandleobject *hobj) +get_thread_handle_state(struct _PyThread_handle_data *data) { - return _Py_atomic_load_int(&hobj->state); + return _Py_atomic_load_int(&data->state); } static inline void -set_thread_handle_state(thandleobject *hobj, _PyThreadHandleState state) +set_thread_handle_state(struct _PyThread_handle_data *data, + _PyThreadHandleState state) { - _Py_atomic_store_int(&hobj->state, state); + _Py_atomic_store_int(&data->state, state); } static int -join_thread(thandleobject *hobj) +_join_thread(struct _PyThread_handle_data *data) { - assert(get_thread_handle_state(hobj) == THREAD_HANDLE_RUNNING); + assert(get_thread_handle_state(data) == THREAD_HANDLE_RUNNING); int err; Py_BEGIN_ALLOW_THREADS - err = PyThread_join_thread(hobj->handle); + err = PyThread_join_thread(data->handle); Py_END_ALLOW_THREADS if (err) { PyErr_SetString(ThreadHandleError, "Failed joining thread"); return -1; } - set_thread_handle_state(hobj, THREAD_HANDLE_JOINED); + set_thread_handle_state(data, THREAD_HANDLE_JOINED); return 0; } - -/****************************************/ -/* _ThreadHandle object C-API functions */ -/****************************************/ - -static void track_thread_handle_for_fork(thandleobject *); - -PyObject * -_PyThreadHandle_NewObject(void) +static int +join_thread(struct _PyThread_handle_data *data) { - _PyEventRc *event = _PyEventRc_New(); - if (event == NULL) { - PyErr_NoMemory(); - return NULL; - } - thandleobject *self = PyObject_New(thandleobject, &_PyThreadHandle_Type); - if (self == NULL) { - _PyEventRc_Decref(event); - return NULL; + if (get_thread_handle_state(data) == THREAD_HANDLE_INVALID) { + PyErr_SetString(PyExc_ValueError, + "the handle is invalid and thus cannot be joined"); + return -1; } - self->ident = 0; - self->handle = 0; - self->thread_is_exiting = event; - self->once = (_PyOnceFlag){0}; - self->state = THREAD_HANDLE_INVALID; - track_thread_handle_for_fork(self); + // We want to perform this check outside of the `_PyOnceFlag` to prevent + // deadlock in the scenario where another thread joins us and we then + // attempt to join ourselves. However, it's not safe to check thread + // identity once the handle's os thread has finished. We may end up reusing + // the identity stored in the handle and erroneously think we are + // attempting to join ourselves. + // + // 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(&data->thread_is_exiting->event) && + data->ident == PyThread_get_thread_ident_ex()) + { + // PyThread_join_thread() would deadlock or error out. + PyErr_SetString(ThreadHandleError, "Cannot join current thread"); + return -1; + } - return (PyObject *)self; + if (_PyOnceFlag_CallOnce( + &data->once, (_Py_once_fn_t *)_join_thread, data) == -1) + { + return -1; + } + assert(get_thread_handle_state(data) == THREAD_HANDLE_JOINED); + return 0; } -void -_PyThreadHandle_SetStarted(PyObject *obj, - PyThread_handle_t handle, PyThread_ident_t ident) +/*********************************/ +/* thread handle C-API functions */ +/*********************************/ + +static void +_PyThread_SetStarted(struct _PyThread_handle_data *data, + PyThread_handle_t handle, PyThread_ident_t ident) { - thandleobject *hobj = (thandleobject *)obj; - assert(get_thread_handle_state(hobj) == THREAD_HANDLE_INVALID); - hobj->handle = handle; - hobj->ident = ident; - set_thread_handle_state(hobj, THREAD_HANDLE_RUNNING); + assert(get_thread_handle_state(data) == THREAD_HANDLE_INVALID); + data->handle = handle; + data->ident = ident; + set_thread_handle_state(data, THREAD_HANDLE_RUNNING); } -_PyEventRc * -_PyThreadHandle_GetExitingEvent(PyObject *hobj) +static _PyEventRc * +_PyThread_GetExitingEvent(struct _PyThread_handle_data *data) { - return ((thandleobject *)hobj)->thread_is_exiting; + return data->thread_is_exiting; } -/************************************/ -/* tracking thread handles for fork */ -/************************************/ +/*************************/ +/* _ThreadHandle objects */ +/*************************/ -static void -track_thread_handle_for_fork(thandleobject *hobj) -{ - HEAD_LOCK(&_PyRuntime); - llist_insert_tail(&_PyRuntime.threads.handles, &hobj->fork_node); - HEAD_UNLOCK(&_PyRuntime); -} +typedef struct { + PyObject_HEAD + struct llist_node fork_node; // linked list node (see _pythread_runtime_state) -static void -untrack_thread_handle_for_fork(thandleobject *hobj) + struct _PyThread_handle_data *data; +} thandleobject; + + +static void track_thread_handle_for_fork(thandleobject *); + +PyObject * +_PyThreadHandle_NewObject(void) { - HEAD_LOCK(&_PyRuntime); - if (hobj->fork_node.next != NULL) { - llist_remove(&hobj->fork_node); + thandleobject *self = PyObject_New(thandleobject, &_PyThreadHandle_Type); + if (self == NULL) { + return NULL; + } + self->data = new_thread_handle_data(); + if (self->data == NULL) { + Py_DECREF(self); + return NULL; } - HEAD_UNLOCK(&_PyRuntime); -} -static void -clear_tracked_thread_handles(struct _pythread_runtime_state *state, - PyThread_ident_t current) -{ - struct llist_node *node; - llist_for_each_safe(node, &state->handles) { - thandleobject *hobj = llist_data(node, thandleobject, fork_node); - if (hobj->ident == current) { - continue; - } + track_thread_handle_for_fork(self); - // Disallow calls to join() as they could crash. We are the only - // thread; it's safe to set this without an atomic. - hobj->state = THREAD_HANDLE_INVALID; - llist_remove(node); - } + return (PyObject *)self; } -/**********************************/ /* _ThreadHandle instance methods */ -/**********************************/ static PyObject * ThreadHandle_join(thandleobject *self, void* ignored) { - if (get_thread_handle_state(self) == THREAD_HANDLE_INVALID) { - PyErr_SetString(PyExc_ValueError, - "the handle is invalid and thus cannot be joined"); + if (join_thread(self->data) < 0) { return NULL; } - - // We want to perform this check outside of the `_PyOnceFlag` to prevent - // deadlock in the scenario where another thread joins us and we then - // attempt to join ourselves. However, it's not safe to check thread - // identity once the handle's os thread has finished. We may end up reusing - // the identity stored in the handle and erroneously think we are - // attempting to join ourselves. - // - // 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->event) && - self->ident == PyThread_get_thread_ident_ex()) { - // PyThread_join_thread() would deadlock or error out. - PyErr_SetString(ThreadHandleError, "Cannot join current thread"); - return NULL; - } - - if (_PyOnceFlag_CallOnce(&self->once, (_Py_once_fn_t *)join_thread, - self) == -1) { - return NULL; - } - assert(get_thread_handle_state(self) == THREAD_HANDLE_JOINED); Py_RETURN_NONE; } @@ -216,14 +233,12 @@ static PyMethodDef ThreadHandle_methods[] = }; -/*************************************/ /* _ThreadHandle instance properties */ -/*************************************/ static PyObject * ThreadHandle_get_ident(thandleobject *self, void *ignored) { - return PyLong_FromUnsignedLongLong(self->ident); + return PyLong_FromUnsignedLongLong(self->data->ident); } @@ -233,15 +248,13 @@ static PyGetSetDef ThreadHandle_getsetlist[] = { }; -/***************************/ /* The _ThreadHandle class */ -/***************************/ + +static void untrack_thread_handle_for_fork(thandleobject *); static void ThreadHandle_dealloc(thandleobject *self) { - PyObject *tp = (PyObject *) Py_TYPE(self); - // Remove ourself from the global list of handles untrack_thread_handle_for_fork(self); @@ -249,17 +262,7 @@ ThreadHandle_dealloc(thandleobject *self) // 1. This is the destructor; nothing else holds a reference. // 2. The refcount going to zero is a "synchronizes-with" event; // all changes from other threads are visible. - if (self->state == THREAD_HANDLE_RUNNING) { - // This is typically short so no need to release the GIL - if (PyThread_detach_thread(self->handle)) { - PyErr_SetString(ThreadHandleError, "Failed detaching thread"); - PyErr_WriteUnraisable(tp); - } - else { - set_thread_handle_state(self, THREAD_HANDLE_DETACHED); - } - } - _PyEventRc_Decref(self->thread_is_exiting); + free_thread_handle_data(self->data); PyObject_Free(self); } @@ -268,7 +271,7 @@ static PyObject * ThreadHandle_repr(thandleobject *self) { return PyUnicode_FromFormat("<%s object: ident=%" PY_FORMAT_THREAD_IDENT_T ">", - Py_TYPE(self)->tp_name, self->ident); + Py_TYPE(self)->tp_name, self->data->ident); } @@ -284,10 +287,70 @@ PyTypeObject _PyThreadHandle_Type = { }; +/************************************/ +/* tracking thread handles for fork */ +/************************************/ + +// XXX Track the handles instead of the objects. + +static void +track_thread_handle_for_fork(thandleobject *hobj) +{ + HEAD_LOCK(&_PyRuntime); + llist_insert_tail(&_PyRuntime.threads.handles, &hobj->fork_node); + HEAD_UNLOCK(&_PyRuntime); +} + +static void +untrack_thread_handle_for_fork(thandleobject *hobj) +{ + HEAD_LOCK(&_PyRuntime); + if (hobj->fork_node.next != NULL) { + llist_remove(&hobj->fork_node); + } + HEAD_UNLOCK(&_PyRuntime); +} + +static void +clear_tracked_thread_handles(struct _pythread_runtime_state *state, + PyThread_ident_t current) +{ + struct llist_node *node; + llist_for_each_safe(node, &state->handles) { + thandleobject *hobj = llist_data(node, thandleobject, fork_node); + if (hobj->data != NULL) { + if (hobj->data->ident == current) { + continue; + } + + // Disallow calls to join() as they could crash. We are the only + // thread; it's safe to set this without an atomic. + hobj->data->state = THREAD_HANDLE_INVALID; + } + llist_remove(node); + } +} + + /*************/ /* other API */ /*************/ +void +_PyThreadHandle_SetStarted(PyObject *hobj, + PyThread_handle_t handle, PyThread_ident_t ident) +{ + _PyThread_SetStarted(((thandleobject *)hobj)->data, handle, ident); +} + + +_PyEventRc * +_PyThreadHandle_GetExitingEvent(PyObject *hobj) +{ + return _PyThread_GetExitingEvent(((thandleobject *)hobj)->data); +} + + void _PyThread_AfterFork(struct _pythread_runtime_state *state) { From f1e8fa8ec4586db3024f846afc7703ce39ea6767 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 13 Mar 2024 20:20:35 -0600 Subject: [PATCH 14/14] Track the data, not the objects. --- Include/internal/pycore_pythread.h | 2 +- Objects/threadhandleobject.c | 48 ++++++++++++++---------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/Include/internal/pycore_pythread.h b/Include/internal/pycore_pythread.h index 39c666ac026132..8bfb89cd25a38e 100644 --- a/Include/internal/pycore_pythread.h +++ b/Include/internal/pycore_pythread.h @@ -78,7 +78,7 @@ struct _pythread_runtime_state { } stubs; #endif - // Linked list of ThreadHandleObjects + // Linked list of struct _PyThread_handle_data struct llist_node handles; }; diff --git a/Objects/threadhandleobject.c b/Objects/threadhandleobject.c index e0d6f6770c96f5..138fa36836f82a 100644 --- a/Objects/threadhandleobject.c +++ b/Objects/threadhandleobject.c @@ -24,6 +24,8 @@ // their turn to execute or an earlier operation completes successfully. Once a // join has completed successfully all future joins complete immediately. struct _PyThread_handle_data { + struct llist_node fork_node; // linked list node (see _pythread_runtime_state) + // The `ident` and `handle` fields are immutable once the object is visible // to threads other than its creator, thus they do not need to be accessed // atomically. @@ -44,6 +46,8 @@ struct _PyThread_handle_data { }; +static void track_thread_handle_for_fork(struct _PyThread_handle_data *); + static struct _PyThread_handle_data * new_thread_handle_data(void) { @@ -62,13 +66,17 @@ new_thread_handle_data(void) .thread_is_exiting = event, .state = THREAD_HANDLE_INVALID, }; - // XXX Add _PyRuntime.threads.handles here. + + track_thread_handle_for_fork(data); + return data; } static inline int get_thread_handle_state(struct _PyThread_handle_data *); static inline void set_thread_handle_state( struct _PyThread_handle_data *, _PyThreadHandleState); +static void untrack_thread_handle_for_fork(struct _PyThread_handle_data *); + static void free_thread_handle_data(struct _PyThread_handle_data *data) @@ -83,6 +91,7 @@ free_thread_handle_data(struct _PyThread_handle_data *data) set_thread_handle_state(data, THREAD_HANDLE_DETACHED); } } + untrack_thread_handle_for_fork(data); _PyEventRc_Decref(data->thread_is_exiting); PyMem_RawFree(data); } @@ -187,14 +196,11 @@ _PyThread_GetExitingEvent(struct _PyThread_handle_data *data) typedef struct { PyObject_HEAD - struct llist_node fork_node; // linked list node (see _pythread_runtime_state) struct _PyThread_handle_data *data; } thandleobject; -static void track_thread_handle_for_fork(thandleobject *); - PyObject * _PyThreadHandle_NewObject(void) { @@ -208,8 +214,6 @@ _PyThreadHandle_NewObject(void) return NULL; } - track_thread_handle_for_fork(self); - return (PyObject *)self; } @@ -250,14 +254,9 @@ static PyGetSetDef ThreadHandle_getsetlist[] = { /* The _ThreadHandle class */ -static void untrack_thread_handle_for_fork(thandleobject *); - static void ThreadHandle_dealloc(thandleobject *self) { - // Remove ourself from the global list of handles - untrack_thread_handle_for_fork(self); - // It's safe to access state non-atomically: // 1. This is the destructor; nothing else holds a reference. // 2. The refcount going to zero is a "synchronizes-with" event; @@ -294,19 +293,19 @@ PyTypeObject _PyThreadHandle_Type = { // XXX Track the handles instead of the objects. static void -track_thread_handle_for_fork(thandleobject *hobj) +track_thread_handle_for_fork(struct _PyThread_handle_data *data) { HEAD_LOCK(&_PyRuntime); - llist_insert_tail(&_PyRuntime.threads.handles, &hobj->fork_node); + llist_insert_tail(&_PyRuntime.threads.handles, &data->fork_node); HEAD_UNLOCK(&_PyRuntime); } static void -untrack_thread_handle_for_fork(thandleobject *hobj) +untrack_thread_handle_for_fork(struct _PyThread_handle_data *data) { HEAD_LOCK(&_PyRuntime); - if (hobj->fork_node.next != NULL) { - llist_remove(&hobj->fork_node); + if (data->fork_node.next != NULL) { + llist_remove(&data->fork_node); } HEAD_UNLOCK(&_PyRuntime); } @@ -317,16 +316,15 @@ clear_tracked_thread_handles(struct _pythread_runtime_state *state, { struct llist_node *node; llist_for_each_safe(node, &state->handles) { - thandleobject *hobj = llist_data(node, thandleobject, fork_node); - if (hobj->data != NULL) { - if (hobj->data->ident == current) { - continue; - } - - // Disallow calls to join() as they could crash. We are the only - // thread; it's safe to set this without an atomic. - hobj->data->state = THREAD_HANDLE_INVALID; + struct _PyThread_handle_data *data = llist_data( + node, struct _PyThread_handle_data, fork_node); + if (data->ident == current) { + continue; } + + // Disallow calls to join() as they could crash. We are the only + // thread; it's safe to set this without an atomic. + data->state = THREAD_HANDLE_INVALID; llist_remove(node); } }