diff --git a/Include/internal/pycore_pythread.h b/Include/internal/pycore_pythread.h index d2e7cc2a206ced..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; }; @@ -153,6 +153,31 @@ 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; + +extern PyTypeObject _PyThreadHandle_Type; + +extern PyObject * _PyThreadHandle_NewObject(void); +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 cc5396a035018f..1072cf649269d4 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" @@ -25,13 +25,16 @@ // Forward declarations static struct PyModuleDef thread_module; -// Module state + +/****************/ +/* Module state */ +/****************/ + typedef struct { PyTypeObject *excepthook_type; PyTypeObject *lock_type; PyTypeObject *local_type; PyTypeObject *local_dummy_type; - PyTypeObject *thread_handle_type; } thread_module_state; static inline thread_module_state* @@ -42,232 +45,226 @@ get_thread_state(PyObject *module) return (thread_module_state *)state; } -// _ThreadHandle type - -// 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, -} ThreadHandleState; - -// 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 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 a value from the `ThreadHandleState` enum. - int state; +/********************/ +/* thread execution */ +/********************/ - // 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. +// 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; +}; - // Serializes calls to `join`. - _PyOnceFlag once; -} ThreadHandleObject; - -static inline int -get_thread_handle_state(ThreadHandleObject *handle) -{ - return _Py_atomic_load_int(&handle->state); -} - -static inline void -set_thread_handle_state(ThreadHandleObject *handle, ThreadHandleState state) -{ - _Py_atomic_store_int(&handle->state, state); -} - -static ThreadHandleObject* -new_thread_handle(thread_module_state* state) +static struct bootstate * +thread_bootstate_new(PyObject *func, PyObject *args, PyObject *kwargs, + _PyEventRc *thread_is_exiting) { - _PyEventRc *event = _PyEventRc_New(); - if (event == NULL) { - PyErr_NoMemory(); + PyInterpreterState *interp = _PyInterpreterState_GET(); + if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_THREADS)) { + PyErr_SetString(PyExc_RuntimeError, + "thread is not supported for isolated subinterpreters"); return NULL; } - ThreadHandleObject* self = PyObject_New(ThreadHandleObject, state->thread_handle_type); - if (self == NULL) { - _PyEventRc_Decref(event); + if (interp->finalizing) { + PyErr_SetString(PyExc_PythonFinalizationError, + "can't create new thread at interpreter shutdown"); return NULL; } - self->ident = 0; - self->handle = 0; - self->thread_is_exiting = event; - self->once = (_PyOnceFlag){0}; - self->state = THREAD_HANDLE_INVALID; - - HEAD_LOCK(&_PyRuntime); - llist_insert_tail(&_PyRuntime.threads.handles, &self->node); - HEAD_UNLOCK(&_PyRuntime); - - return 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; + PyThreadState *tstate = _PyThreadState_New( + interp, _PyThreadState_WHENCE_THREADING); + if (tstate == NULL) { + if (!PyErr_Occurred()) { + PyErr_NoMemory(); } + return NULL; } - _PyEventRc_Decref(self->thread_is_exiting); - PyObject_Free(self); - Py_DECREF(tp); -} -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) { - ThreadHandleObject *hobj = llist_data(node, ThreadHandleObject, node); - if (hobj->ident == current) { - continue; + if (args == NULL) { + args = PyTuple_New(0); + if (args == NULL) { + PyThreadState_Clear(tstate); + PyThreadState_Delete(tstate); } + } + else { + Py_INCREF(args); + } - // 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); + // 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 PyObject * -ThreadHandle_repr(ThreadHandleObject *self) +static void +thread_bootstate_free(struct bootstate *boot) { - return PyUnicode_FromFormat("<%s object: ident=%" PY_FORMAT_THREAD_IDENT_T ">", - Py_TYPE(self)->tp_name, self->ident); + 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 PyObject * -ThreadHandle_get_ident(ThreadHandleObject *self, void *ignored) +static void +thread_bootstate_finalizing(struct bootstate *boot) { - return PyLong_FromUnsignedLongLong(self->ident); + // 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 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; -} -static PyObject * -ThreadHandle_join(ThreadHandleObject *self, void* ignored) +static void +thread_run(void *boot_raw) { - if (get_thread_handle_state(self) == THREAD_HANDLE_INVALID) { - PyErr_SetString(PyExc_ValueError, - "the handle is invalid and thus cannot be joined"); - return NULL; - } + 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; - // 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. + // 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. // - // 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(ThreadError, "Cannot join current thread"); - return NULL; + // 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; } - if (_PyOnceFlag_CallOnce(&self->once, (_Py_once_fn_t *)join_thread, - self) == -1) { - return NULL; + _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); + } } - assert(get_thread_handle_state(self) == THREAD_HANDLE_JOINED); - Py_RETURN_NONE; + 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 PyGetSetDef ThreadHandle_getsetlist[] = { - {"ident", (getter)ThreadHandle_get_ident, NULL, NULL}, - {0}, -}; -static PyMethodDef ThreadHandle_methods[] = +static void _lock_unlock(PyObject *); + +static void +release_sentinel(void *weakref_raw) { - {"join", (PyCFunction)ThreadHandle_join, METH_NOARGS}, - {0, 0} -}; + PyObject *weakref = _PyObject_CAST(weakref_raw); -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} -}; + /* 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); + } -static PyType_Spec ThreadHandle_Type_spec = { - "_thread._ThreadHandle", - sizeof(ThreadHandleObject), - 0, - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, - ThreadHandle_Type_slots, -}; + /* 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 */ +/****************/ typedef struct { PyObject_HEAD @@ -381,6 +378,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)) { @@ -519,7 +526,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 +862,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,292 +1283,111 @@ _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 -// limitations, are contained in bootstate. -struct bootstate { - PyThreadState *tstate; - PyObject *func; - PyObject *args; - PyObject *kwargs; - _PyEventRc *thread_is_exiting; -}; +/********************/ +/* Module functions */ +/********************/ +/* runtime state inquiries */ -static void -thread_bootstate_free(struct bootstate *boot, int decref) +static PyObject * +threadmod_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored)) { - if (decref) { - Py_DECREF(boot->func); - Py_DECREF(boot->args); - Py_XDECREF(boot->kwargs); + PyInterpreterState *interp = _PyInterpreterState_GET(); + if (interp->feature_flags & Py_RTFLAGS_DAEMON_THREADS) { + Py_RETURN_TRUE; } - if (boot->thread_is_exiting != NULL) { - _PyEventRc_Decref(boot->thread_is_exiting); + else { + Py_RETURN_FALSE; } - PyMem_RawFree(boot); } +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 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)) { - // 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); - 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); - } - - thread_bootstate_free(boot, 1); - - _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 PyObject * -thread_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored)) +threadmod__count(PyObject *self, PyObject *Py_UNUSED(ignored)) { PyInterpreterState *interp = _PyInterpreterState_GET(); - if (interp->feature_flags & Py_RTFLAGS_DAEMON_THREADS) { - Py_RETURN_TRUE; - } - else { - Py_RETURN_FALSE; - } + return PyLong_FromSsize_t(_Py_atomic_load_ssize(&interp->threads.count)); } -PyDoc_STRVAR(daemon_threads_allowed_doc, -"daemon_threads_allowed()\n\ +PyDoc_STRVAR(_count_doc, +"_count() -> integer\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, - int joinable, - 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)); - 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); - } +\ +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\ +This function is meant for internal and specialized purposes only.\n\ +In most applications `threading.enumerate()` should be used instead."); - int err; - if (joinable) { - 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); - } - if (err) { - PyErr_SetString(ThreadError, "can't start new thread"); - PyThreadState_Clear(boot->tstate); - thread_bootstate_free(boot, 1); - return -1; - } - return 0; -} static PyObject * -thread_PyThread_start_new_thread(PyObject *module, PyObject *fargs) +threadmod_stack_size(PyObject *self, PyObject *args) { - 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; - } + size_t old_size; + Py_ssize_t new_size = 0; + int rc; - if (PySys_Audit("_thread.start_new_thread", "OOO", - func, args, kwargs ? kwargs : Py_None) < 0) { + if (!PyArg_ParseTuple(args, "|n:stack_size", &new_size)) 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 (new_size < 0) { + PyErr_SetString(PyExc_ValueError, + "size must be 0 or a positive value"); return NULL; } - return PyLong_FromUnsignedLongLong(ident); -} -PyDoc_STRVAR(start_new_doc, -"start_new_thread(function, args[, kwargs])\n\ -(start_new() is an obsolete synonym)\n\ -\n\ -Start a new thread and return its identifier.\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"); - -static PyObject * -thread_PyThread_start_joinable_thread(PyObject *module, PyObject *func) -{ - thread_module_state *state = get_thread_state(module); + old_size = PyThread_get_stacksize(); - if (!PyCallable_Check(func)) { - PyErr_SetString(PyExc_TypeError, - "thread function must be callable"); + 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 (PySys_Audit("_thread.start_joinable_thread", "O", func) < 0) { + if (rc == -2) { + PyErr_SetString(ThreadError, + "setting stack size not supported"); 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, - &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\ +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 * -thread_PyThread_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 * -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)) { @@ -1580,21 +1412,11 @@ A subthread can use this function to interrupt the main thread.\n\ Note: the default signal handler for SIGINT raises ``KeyboardInterrupt``." ); -static PyObject * -thread_PyThread_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 * -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) { @@ -1615,9 +1437,10 @@ 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 * -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); @@ -1631,139 +1454,167 @@ by the OS (kernel). This may be used to uniquely identify a\n\ particular thread within a system."); #endif + static PyObject * -thread__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\ -\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\ +PyDoc_STRVAR(thread__is_main_interpreter_doc, +"_is_main_interpreter()\n\ \n\ -This function is meant for internal and specialized purposes only.\n\ -In most applications `threading.enumerate()` should be used instead."); +Return True if the current interpreter is the main Python interpreter."); -static void -release_sentinel(void *weakref_raw) + +static PyObject * +threadmod_exit_thread(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *weakref = _PyObject_CAST(weakref_raw); + PyErr_SetNone(PyExc_SystemExit); + return NULL; +} - /* 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); - } +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."); - /* Deallocating a weakref with a NULL callback only calls - PyObject_GC_Del(), which can't call any Python code. */ - Py_DECREF(weakref); -} + +/* thread execution */ static PyObject * -thread__set_sentinel(PyObject *module, PyObject *Py_UNUSED(ignored)) +threadmod_start_new_thread(PyObject *module, PyObject *fargs) { - PyObject *wr; - PyThreadState *tstate = _PyThreadState_GET(); - lockobject *lock; + PyObject *func, *args, *kwargs = NULL; + 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 (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); + if (PySys_Audit("_thread.start_new_thread", "OOO", + func, args, kwargs ? kwargs : Py_None) < 0) { + return NULL; } - lock = newlockobject(module); - if (lock == NULL) + + struct bootstate *boot = thread_bootstate_new(func, args, kwargs, NULL); + if (boot == 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) { - Py_DECREF(lock); + } + + 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; } - tstate->on_delete_data = (void *) wr; - tstate->on_delete = &release_sentinel; - return (PyObject *) lock; + 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 * -thread_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; + if (!PyCallable_Check(func)) { + PyErr_SetString(PyExc_TypeError, + "thread function must be callable"); + return NULL; + } - if (!PyArg_ParseTuple(args, "|n:stack_size", &new_size)) + if (PySys_Audit("_thread.start_joinable_thread", "O", func) < 0) { return NULL; + } - if (new_size < 0) { - PyErr_SetString(PyExc_ValueError, - "size must be 0 or a positive value"); + PyObject *hobj = _PyThreadHandle_NewObject(); + if (hobj == NULL) { + return NULL; + } + struct bootstate *boot = thread_bootstate_new( + 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, &ident, &handle) < 0) + { + PyErr_SetString(ThreadError, "can't start new thread"); + thread_bootstate_free(boot); + Py_DECREF(hobj); + return NULL; + } + _PyThreadHandle_SetStarted(hobj, handle, ident); + return hobj; +} - old_size = PyThread_get_stacksize(); +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"); - rc = PyThread_set_stacksize((size_t) new_size); - if (rc == -1) { - PyErr_Format(PyExc_ValueError, - "size not valid: %zd bytes", - new_size); + +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 (rc == -2) { - PyErr_SetString(ThreadError, - "setting stack size not supported"); + if (set_threadstate_finalizer(tstate, lock) < 0) { + Py_DECREF(lock); return NULL; } - - return PyLong_FromSsize_t((Py_ssize_t) old_size); + 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, @@ -1844,7 +1695,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); @@ -1905,58 +1756,65 @@ PyDoc_STRVAR(excepthook_doc, \n\ Handle uncaught Thread.run() exception."); + +/* other */ + static PyObject * -thread__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)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 */ }; +/***************************/ /* Initialization function */ +/***************************/ static int thread_module_exec(PyObject *module) @@ -1968,11 +1826,9 @@ thread_module_exec(PyObject *module) PyThread_init_thread(); // _ThreadHandle - state->thread_handle_type = (PyTypeObject *)PyType_FromSpec(&ThreadHandle_Type_spec); - 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; } @@ -2053,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; } @@ -2065,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 new file mode 100644 index 00000000000000..138fa36836f82a --- /dev/null +++ b/Objects/threadhandleobject.c @@ -0,0 +1,361 @@ +/* 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 + + +/**********************/ +/* thread handle data */ +/**********************/ + +// 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. +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. + PyThread_ident_t ident; + PyThread_handle_t handle; + + // Holds a value from the `ThreadHandleState` enum. + 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; +}; + + +static void track_thread_handle_for_fork(struct _PyThread_handle_data *); + +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, + }; + + 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) +{ + 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); + } + } + untrack_thread_handle_for_fork(data); + _PyEventRc_Decref(data->thread_is_exiting); + PyMem_RawFree(data); +} + + +/***************************/ +/* internal implementation */ +/***************************/ + +static inline int +get_thread_handle_state(struct _PyThread_handle_data *data) +{ + return _Py_atomic_load_int(&data->state); +} + +static inline void +set_thread_handle_state(struct _PyThread_handle_data *data, + _PyThreadHandleState state) +{ + _Py_atomic_store_int(&data->state, state); +} + +static int +_join_thread(struct _PyThread_handle_data *data) +{ + assert(get_thread_handle_state(data) == THREAD_HANDLE_RUNNING); + + int err; + Py_BEGIN_ALLOW_THREADS + err = PyThread_join_thread(data->handle); + Py_END_ALLOW_THREADS + if (err) { + PyErr_SetString(ThreadHandleError, "Failed joining thread"); + return -1; + } + set_thread_handle_state(data, THREAD_HANDLE_JOINED); + return 0; +} + +static int +join_thread(struct _PyThread_handle_data *data) +{ + if (get_thread_handle_state(data) == THREAD_HANDLE_INVALID) { + PyErr_SetString(PyExc_ValueError, + "the handle is invalid and thus cannot be joined"); + return -1; + } + + // 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; + } + + 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; +} + + +/*********************************/ +/* thread handle C-API functions */ +/*********************************/ + +static void +_PyThread_SetStarted(struct _PyThread_handle_data *data, + PyThread_handle_t handle, PyThread_ident_t ident) +{ + assert(get_thread_handle_state(data) == THREAD_HANDLE_INVALID); + data->handle = handle; + data->ident = ident; + set_thread_handle_state(data, THREAD_HANDLE_RUNNING); +} + + +static _PyEventRc * +_PyThread_GetExitingEvent(struct _PyThread_handle_data *data) +{ + return data->thread_is_exiting; +} + + +/*************************/ +/* _ThreadHandle objects */ +/*************************/ + +typedef struct { + PyObject_HEAD + + struct _PyThread_handle_data *data; +} thandleobject; + + +PyObject * +_PyThreadHandle_NewObject(void) +{ + 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; + } + + return (PyObject *)self; +} + + +/* _ThreadHandle instance methods */ + +static PyObject * +ThreadHandle_join(thandleobject *self, void* ignored) +{ + if (join_thread(self->data) < 0) { + return NULL; + } + 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->data->ident); +} + + +static PyGetSetDef ThreadHandle_getsetlist[] = { + {"ident", (getter)ThreadHandle_get_ident, NULL, NULL}, + {0}, +}; + + +/* The _ThreadHandle class */ + +static void +ThreadHandle_dealloc(thandleobject *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. + free_thread_handle_data(self->data); + PyObject_Free(self); +} + + +static PyObject * +ThreadHandle_repr(thandleobject *self) +{ + return PyUnicode_FromFormat("<%s object: ident=%" PY_FORMAT_THREAD_IDENT_T ">", + Py_TYPE(self)->tp_name, self->data->ident); +} + + +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, +}; + + +/************************************/ +/* tracking thread handles for fork */ +/************************************/ + +// XXX Track the handles instead of the objects. + +static void +track_thread_handle_for_fork(struct _PyThread_handle_data *data) +{ + HEAD_LOCK(&_PyRuntime); + llist_insert_tail(&_PyRuntime.threads.handles, &data->fork_node); + HEAD_UNLOCK(&_PyRuntime); +} + +static void +untrack_thread_handle_for_fork(struct _PyThread_handle_data *data) +{ + HEAD_LOCK(&_PyRuntime); + if (data->fork_node.next != NULL) { + llist_remove(&data->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) { + 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); + } +} + + +/*************/ +/* 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) +{ + // 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