From 1828b9a8ec7a1a962374a81086eb4c899a152bb6 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 24 Apr 2025 16:59:16 -0400 Subject: [PATCH 01/61] Implement interpreter state reference counting. --- Include/cpython/pystate.h | 9 +++++++ Include/internal/pycore_interp_structs.h | 4 +++ Python/pylifecycle.c | 1 + Python/pystate.c | 33 ++++++++++++++++++++++-- 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index c562426767c2cb..86072a0b9c9403 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -265,3 +265,12 @@ PyAPI_FUNC(_PyFrameEvalFunction) _PyInterpreterState_GetEvalFrameFunc( PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameFunc( PyInterpreterState *interp, _PyFrameEvalFunction eval_frame); + +/* Similar to PyInterpreterState_Get(), but returns the interpreter with an + * incremented reference count. PyInterpreterState_Delete() won't delete the + * full interpreter structure until the reference is released by + * PyThreadState_Ensure() or PyInterpreterState_Release(). */ +PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Hold(); + +/* Release a reference to an interpreter incremented by PyInterpreterState_Hold() */ +PyAPI_FUNC(void) PyInterpreterState_Release(PyInterpreterState *interp); diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index af6ee3ab48939f..4783b40e0cf86a 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -949,6 +949,10 @@ struct _is { # endif #endif + /* This prevents the interpreter from deleting. + See PEP 788 for the full specification. */ + Py_ssize_t refcount; + /* the initial PyInterpreterState.threads.head */ _PyThreadStateImpl _initial_thread; // _initial_thread should be the last field of PyInterpreterState. diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 1b9832bff17ba5..fb8ef7f48a8798 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -2288,6 +2288,7 @@ new_interpreter(PyThreadState **tstate_p, } _PyInterpreterState_SetWhence(interp, whence); interp->_ready = 1; + interp->refcount = 1; // XXX Might new_interpreter() have been called without the GIL held? PyThreadState *save_tstate = _PyThreadState_GET(); diff --git a/Python/pystate.c b/Python/pystate.c index aba558279a657d..0bdb605a0e77d3 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -600,6 +600,15 @@ free_interpreter(PyInterpreterState *interp) } } +static void +decref_interpreter(PyInterpreterState *interp) +{ + assert(interp != NULL); + if (_Py_atomic_add_ssize(&interp->refcount, -1) == 1) { + free_interpreter(interp); + } +} + #ifndef NDEBUG static inline int check_interpreter_whence(long); #endif @@ -793,6 +802,7 @@ _PyInterpreterState_New(PyThreadState *tstate, PyInterpreterState **pinterp) HEAD_UNLOCK(runtime); if (interp != NULL) { + assert(interp->refcount == 1); free_interpreter(interp); } return status; @@ -1043,7 +1053,7 @@ PyInterpreterState_Delete(PyInterpreterState *interp) _PyObject_FiniState(interp); - free_interpreter(interp); + decref_interpreter(interp); } @@ -1079,7 +1089,7 @@ _PyInterpreterState_DeleteExceptMain(_PyRuntimeState *runtime) zapthreads(interp); PyInterpreterState *prev_interp = interp; interp = interp->next; - free_interpreter(prev_interp); + decref_interpreter(prev_interp); } HEAD_UNLOCK(runtime); @@ -3185,3 +3195,22 @@ _Py_GetMainConfig(void) } return _PyInterpreterState_GetConfig(interp); } + +PyInterpreterState * +PyInterpreterState_Hold(void) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + assert(_Py_atomic_load_ssize_relaxed(&interp->refcount) > 0); + _Py_atomic_add_ssize(&interp->refcount, 1); + return interp; +} + +void +PyInterpreterState_Release(PyInterpreterState *interp) +{ + PyThreadState *tstate = PyThreadState_Get(); + if (tstate->interp != interp) { + Py_FatalError("thread state has the wrong interpreter"); + } + decref_interpreter(interp); +} From d0895f9be8e267120ed8eea0da72a34df7a876f9 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 24 Apr 2025 17:12:27 -0400 Subject: [PATCH 02/61] Add a test for interpreter reference counts. --- Include/cpython/pystate.h | 5 ++++- Modules/_testcapimodule.c | 25 +++++++++++++++++++++++++ Python/pystate.c | 14 ++++++++++---- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 86072a0b9c9403..867d070a110283 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -270,7 +270,10 @@ PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameFunc( * incremented reference count. PyInterpreterState_Delete() won't delete the * full interpreter structure until the reference is released by * PyThreadState_Ensure() or PyInterpreterState_Release(). */ -PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Hold(); +PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Hold(void); /* Release a reference to an interpreter incremented by PyInterpreterState_Hold() */ PyAPI_FUNC(void) PyInterpreterState_Release(PyInterpreterState *interp); + +// Export for '_testcapi' shared extension +PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 3aa6e4c9e43a26..bde04705aa6826 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2546,6 +2546,30 @@ toggle_reftrace_printer(PyObject *ob, PyObject *arg) Py_RETURN_NONE; } +static PyObject * +test_interp_refcount(PyObject *self, PyObject *unused) +{ + PyThreadState *save = PyThreadState_Get(); + PyThreadState *tstate = Py_NewInterpreter(); + assert(tstate == PyThreadState_Get()); + PyInterpreterState *interp = PyThreadState_GetInterpreter(tstate); + assert(interp != NULL); + + assert(_PyInterpreterState_Refcount(interp) == 1); + PyInterpreterState *held = PyInterpreterState_Hold(); + assert(_PyInterpreterState_Refcount(interp) == 2); + PyInterpreterState_Release(held); + assert(_PyInterpreterState_Refcount(interp) == 1); + + held = PyInterpreterState_Hold(); + Py_EndInterpreter(tstate); + PyThreadState_Swap(save); + assert(_PyInterpreterState_Refcount(interp) == 1); + PyInterpreterState_Release(held); + + Py_RETURN_NONE; +} + static PyMethodDef TestMethods[] = { {"set_errno", set_errno, METH_VARARGS}, {"test_config", test_config, METH_NOARGS}, @@ -2640,6 +2664,7 @@ static PyMethodDef TestMethods[] = { {"test_atexit", test_atexit, METH_NOARGS}, {"code_offset_to_line", _PyCFunction_CAST(code_offset_to_line), METH_FASTCALL}, {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, + {"test_interp_refcount", test_interp_refcount, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/Python/pystate.c b/Python/pystate.c index 0bdb605a0e77d3..cd37b12c74deac 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3196,6 +3196,15 @@ _Py_GetMainConfig(void) return _PyInterpreterState_GetConfig(interp); } +Py_ssize_t +_PyInterpreterState_Refcount(PyInterpreterState *interp) +{ + assert(interp != NULL); + Py_ssize_t refcount = _Py_atomic_load_ssize_relaxed(&interp->refcount); + assert(refcount > 0); + return refcount; +} + PyInterpreterState * PyInterpreterState_Hold(void) { @@ -3208,9 +3217,6 @@ PyInterpreterState_Hold(void) void PyInterpreterState_Release(PyInterpreterState *interp) { - PyThreadState *tstate = PyThreadState_Get(); - if (tstate->interp != interp) { - Py_FatalError("thread state has the wrong interpreter"); - } + assert(interp != NULL); decref_interpreter(interp); } From 5c3ee8da16ec9a7c245713b2ff40d6fdfeeb8f96 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 24 Apr 2025 20:00:27 -0400 Subject: [PATCH 03/61] Add thread state daemon-ness that doesn't work yet. --- Include/cpython/pystate.h | 3 +++ Python/pystate.c | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 867d070a110283..9d3a5b85d15e7f 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -210,6 +210,9 @@ struct _ts { */ PyObject *threading_local_sentinel; _PyRemoteDebuggerSupport remote_debugger_support; + + /* Whether this thread hangs when the interpreter is finalizing. */ + uint8_t daemon; }; # define Py_C_RECURSION_LIMIT 5000 diff --git a/Python/pystate.c b/Python/pystate.c index cd37b12c74deac..851a0ac8f80c1e 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1612,6 +1612,7 @@ new_threadstate(PyInterpreterState *interp, int whence) return NULL; } #endif + ((PyThreadState *)tstate)->daemon = 1; /* We serialize concurrent creation to protect global state. */ HEAD_LOCK(interp->runtime); @@ -2129,7 +2130,7 @@ tstate_wait_attach(PyThreadState *tstate) _PyParkingLot_Park(&tstate->state, &state, sizeof(tstate->state), /*timeout=*/-1, NULL, /*detach=*/0); } - else if (state == _Py_THREAD_SHUTTING_DOWN) { + else if (state == _Py_THREAD_SHUTTING_DOWN && tstate->daemon) { // We're shutting down, so we can't attach. _PyThreadState_HangThread(tstate); } @@ -3069,6 +3070,9 @@ _PyThreadState_CheckConsistency(PyThreadState *tstate) int _PyThreadState_MustExit(PyThreadState *tstate) { + if (!tstate->daemon) { + return 0; + } int state = _Py_atomic_load_int_relaxed(&tstate->state); return state == _Py_THREAD_SHUTTING_DOWN; } @@ -3220,3 +3224,17 @@ PyInterpreterState_Release(PyInterpreterState *interp) assert(interp != NULL); decref_interpreter(interp); } + +int +PyThreadState_SetDaemon(int daemon) +{ + PyThreadState *tstate = PyThreadState_Get(); + if (daemon != 0 && daemon != 1) { + Py_FatalError("daemon must be 0 or 1"); + } + if (tstate->daemon == daemon) { + return 0; + } + tstate->daemon = daemon; + return 1; +} From 7b9ac5957e62888ac4c18b480ed7e66973e69db2 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 24 Apr 2025 20:30:40 -0400 Subject: [PATCH 04/61] Add untested implementation of non-daemon native threads. --- Include/internal/pycore_interp_structs.h | 6 ++++++ Python/pylifecycle.c | 26 ++++++++++++++++++++++++ Python/pystate.c | 19 +++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 4783b40e0cf86a..07d4a02a76985c 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -792,6 +792,12 @@ struct _is { or the size specified by the THREAD_STACK_SIZE macro. */ /* Used in Python/thread.c. */ size_t stacksize; + + struct _Py_finalizing_threads { + Py_ssize_t countdown; + PyEvent finished; + PyMutex mutex; + } finalizing; } threads; /* Reference to the _PyRuntime global variable. This field exists diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index fb8ef7f48a8798..4a62bb92b94237 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -13,6 +13,7 @@ #include "pycore_freelist.h" // _PyObject_ClearFreeLists() #include "pycore_global_objects_fini_generated.h" // _PyStaticObjects_CheckRefcnt() #include "pycore_initconfig.h" // _PyStatus_OK() +#include "pycore_interp_structs.h" #include "pycore_long.h" // _PyLong_InitTypes() #include "pycore_object.h" // _PyDebug_PrintTotalRefs() #include "pycore_obmalloc.h" // _PyMem_init_obmalloc() @@ -96,6 +97,7 @@ static PyStatus init_android_streams(PyThreadState *tstate); static PyStatus init_apple_streams(PyThreadState *tstate); #endif static void wait_for_thread_shutdown(PyThreadState *tstate); +static void wait_for_native_shutdown(PyInterpreterState *interp); static void finalize_subinterpreters(void); static void call_ll_exitfuncs(_PyRuntimeState *runtime); @@ -2012,6 +2014,9 @@ _Py_Finalize(_PyRuntimeState *runtime) // Wrap up existing "threading"-module-created, non-daemon threads. wait_for_thread_shutdown(tstate); + // Wrap up non-daemon native threads + wait_for_native_shutdown(tstate->interp); + // Make any remaining pending calls. _Py_FinishPendingCalls(tstate); @@ -2429,6 +2434,9 @@ Py_EndInterpreter(PyThreadState *tstate) // Wrap up existing "threading"-module-created, non-daemon threads. wait_for_thread_shutdown(tstate); + // Wrap up non-daemon native threads + wait_for_native_shutdown(tstate->interp); + // Make any remaining pending calls. _Py_FinishPendingCalls(tstate); @@ -3455,6 +3463,24 @@ wait_for_thread_shutdown(PyThreadState *tstate) Py_DECREF(threading); } +/* Wait for all non-daemon native threads to finish. + See PEP 788. */ +static void +wait_for_native_shutdown(PyInterpreterState *interp) +{ + assert(interp != NULL); + struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; + PyMutex_Lock(&finalizing->mutex); + if (finalizing->countdown == 0) { + PyMutex_Unlock(&finalizing->mutex); + return; + } + PyMutex_Unlock(&finalizing->mutex); + + PyEvent_Wait(&finalizing->finished); + assert(finalizing->countdown == 0); +} + int Py_AtExit(void (*func)(void)) { struct _atexit_runtime_state *state = &_PyRuntime.atexit; diff --git a/Python/pystate.c b/Python/pystate.c index 851a0ac8f80c1e..b2a0264f4b8a93 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1851,6 +1851,13 @@ tstate_delete_common(PyThreadState *tstate, int release_gil) decrement_stoptheworld_countdown(&runtime->stoptheworld); } } + if (tstate->daemon == 0 + && tstate != (PyThreadState *)&interp->_initial_thread) { + struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; + if (--finalizing->countdown == 0) { + _PyEvent_Notify(&finalizing->finished); + } + } #if defined(Py_REF_DEBUG) && defined(Py_GIL_DISABLED) // Add our portion of the total refcount to the interpreter's total. @@ -3232,9 +3239,21 @@ PyThreadState_SetDaemon(int daemon) if (daemon != 0 && daemon != 1) { Py_FatalError("daemon must be 0 or 1"); } + PyInterpreterState *interp = tstate->interp; + assert(interp != NULL); if (tstate->daemon == daemon) { return 0; } + + struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; + PyMutex_Lock(&finalizing->mutex); + if (_PyEvent_IsSet(&finalizing->finished)) { + /* Native threads have already finalized */ + PyMutex_Unlock(&finalizing->mutex); + return -1; + } + ++finalizing->countdown; + PyMutex_Unlock(&finalizing->mutex); tstate->daemon = daemon; return 1; } From 0868c156f381745846f197e84238a0a53114a3a1 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 24 Apr 2025 20:55:12 -0400 Subject: [PATCH 05/61] Add a test for PyThreadState_SetDaemon(). --- Include/cpython/pystate.h | 2 ++ Programs/_testembed.c | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 9d3a5b85d15e7f..63ad2b8ffbf0d6 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -280,3 +280,5 @@ PyAPI_FUNC(void) PyInterpreterState_Release(PyInterpreterState *interp); // Export for '_testcapi' shared extension PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); + +PyAPI_FUNC(int) PyThreadState_SetDaemon(int daemon); diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 6f6d0cae58010e..57717605d5c256 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2341,6 +2341,45 @@ test_get_incomplete_frame(void) return result; } +static void +non_daemon_native(void *arg) +{ + PyEvent *event = (PyEvent *)arg; + PyThreadState *tstate = PyThreadState_New(PyInterpreterState_Main()); + PyThreadState_Swap(tstate); + int res = PyThreadState_SetDaemon(0); + assert(res == 1); + _PyEvent_Notify(event); + const char *code = "import time\n" + "time.sleep(0.2)\n" + "def fib(n):\n" + " if n <= 1:\n" + " return n\n" + " else:\n" + " return fib(n - 1) + fib(n - 2)\n" + "fib(10)"; + res = PyRun_SimpleString(code); + assert(res == 0); + PyThreadState_Clear(tstate); + PyThreadState_Swap(NULL); + PyThreadState_Delete(tstate); +} + +static int +test_non_daemon_native_thread(void) +{ + _testembed_Py_InitializeFromConfig(); + PyThread_handle_t handle; + PyThread_ident_t ident; + PyEvent event; + if (PyThread_start_joinable_thread(non_daemon_native, &event, + &ident, &handle) < 0) { + return -1; + } + PyEvent_Wait(&event); + Py_Finalize(); + return 0; +} /* ********************************************************* * List of test cases and the function that implements it. @@ -2431,6 +2470,7 @@ static struct TestCase TestCases[] = { {"test_frozenmain", test_frozenmain}, #endif {"test_get_incomplete_frame", test_get_incomplete_frame}, + {"test_non_daemon_native_thread", test_non_daemon_native_thread}, {NULL, NULL} }; From e9ea644212620d96f046a0764747da5760a33a04 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 25 Apr 2025 13:22:08 +0000 Subject: [PATCH 06/61] Add untested implementation of Ensure()/Release() that probably doesn't work and isn't thread-safe. --- Include/cpython/pystate.h | 12 ++++ Python/pystate.c | 119 +++++++++++++++++++++++++++++++++----- 2 files changed, 116 insertions(+), 15 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 63ad2b8ffbf0d6..f20ad0fd507bf9 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -63,6 +63,12 @@ typedef struct _stack_chunk { PyObject * data[1]; /* Variable sized */ } _PyStackChunk; +typedef struct _ensured_tstate { + struct _ensured_tstate *next; + PyThreadState *prior_tstate; + uint8_t was_daemon; +} _Py_ensured_tstate; + struct _ts { /* See Python/ceval.c for comments explaining most fields */ @@ -213,6 +219,8 @@ struct _ts { /* Whether this thread hangs when the interpreter is finalizing. */ uint8_t daemon; + + _Py_ensured_tstate *ensured; }; # define Py_C_RECURSION_LIMIT 5000 @@ -282,3 +290,7 @@ PyAPI_FUNC(void) PyInterpreterState_Release(PyInterpreterState *interp); PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); PyAPI_FUNC(int) PyThreadState_SetDaemon(int daemon); + +PyAPI_FUNC(int) PyThreadState_Ensure(PyInterpreterState *interp); + +PyAPI_FUNC(void) PyThreadState_Release(void); diff --git a/Python/pystate.c b/Python/pystate.c index b2a0264f4b8a93..80a84dca1d69cd 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -600,13 +600,16 @@ free_interpreter(PyInterpreterState *interp) } } -static void +static Py_ssize_t decref_interpreter(PyInterpreterState *interp) { assert(interp != NULL); - if (_Py_atomic_add_ssize(&interp->refcount, -1) == 1) { + Py_ssize_t old_refcnt = _Py_atomic_add_ssize(&interp->refcount, -1); + if (old_refcnt == 1) { free_interpreter(interp); } + + return old_refcnt; } #ifndef NDEBUG @@ -1817,6 +1820,16 @@ PyThreadState_Clear(PyThreadState *tstate) static void decrement_stoptheworld_countdown(struct _stoptheworld_state *stw); +static void +decrement_daemon_count(PyInterpreterState *interp) +{ + assert(interp != NULL); + struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; + if (--finalizing->countdown == 0) { + _PyEvent_Notify(&finalizing->finished); + } +} + /* Common code for PyThreadState_Delete() and PyThreadState_DeleteCurrent() */ static void tstate_delete_common(PyThreadState *tstate, int release_gil) @@ -1853,10 +1866,7 @@ tstate_delete_common(PyThreadState *tstate, int release_gil) } if (tstate->daemon == 0 && tstate != (PyThreadState *)&interp->_initial_thread) { - struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; - if (--finalizing->countdown == 0) { - _PyEvent_Notify(&finalizing->finished); - } + decrement_daemon_count(interp); } #if defined(Py_REF_DEBUG) && defined(Py_GIL_DISABLED) @@ -3232,6 +3242,26 @@ PyInterpreterState_Release(PyInterpreterState *interp) decref_interpreter(interp); } +static int +tstate_set_daemon(PyThreadState *tstate, PyInterpreterState *interp, int daemon) +{ + assert(tstate != NULL); + assert(interp != NULL); + assert(tstate->interp == interp); + assert(daemon == 1 || daemon == 0); + struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; + PyMutex_Lock(&finalizing->mutex); + if (_PyEvent_IsSet(&finalizing->finished)) { + /* Native threads have already finalized */ + PyMutex_Unlock(&finalizing->mutex); + return -1; + } + ++finalizing->countdown; + PyMutex_Unlock(&finalizing->mutex); + tstate->daemon = daemon; + return 1; +} + int PyThreadState_SetDaemon(int daemon) { @@ -3241,19 +3271,78 @@ PyThreadState_SetDaemon(int daemon) } PyInterpreterState *interp = tstate->interp; assert(interp != NULL); + if (tstate == &interp->_initial_thread) { + Py_FatalError("thread cannot be the main thread"); + } if (tstate->daemon == daemon) { return 0; } - struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; - PyMutex_Lock(&finalizing->mutex); - if (_PyEvent_IsSet(&finalizing->finished)) { - /* Native threads have already finalized */ - PyMutex_Unlock(&finalizing->mutex); + return tstate_set_daemon(tstate, interp, daemon); +} + +int +PyThreadState_Ensure(PyInterpreterState *interp) +{ + assert(interp != NULL); + _Py_ensured_tstate *entry = PyMem_RawMalloc(sizeof(_Py_ensured_tstate)); + if (entry == NULL) { + decref_interpreter(interp); return -1; } - ++finalizing->countdown; - PyMutex_Unlock(&finalizing->mutex); - tstate->daemon = daemon; - return 1; + PyThreadState *save = _PyThreadState_GET(); + if (save != NULL && save->interp == interp) { + Py_ssize_t refcnt = decref_interpreter(interp); + assert(refcnt > 1); + entry->was_daemon = save->daemon; + entry->next = save->ensured; + entry->prior_tstate = NULL; + if (tstate_set_daemon(save, interp, 0) < 0) { + PyMem_RawFree(entry); + return -1; + } + save->ensured = entry; + return 0; + } + + PyThreadState *tstate = PyThreadState_New(interp); + decref_interpreter(interp); + if (tstate == NULL) { + PyMem_RawFree(entry); + return -1; + } + if (tstate_set_daemon(tstate, interp, 0) < 0) { + PyMem_RawFree(entry); + return -1; + } + entry->was_daemon = 0; + entry->prior_tstate = save; + entry->next = NULL; + tstate->ensured = entry; + + return 0; +} + +void +PyThreadState_Release(void) +{ + PyThreadState *tstate = _PyThreadState_GET(); + _Py_EnsureTstateNotNULL(tstate); + _Py_ensured_tstate *ensured = tstate->ensured; + if (ensured == NULL) { + Py_FatalError("PyThreadState_Release() called without PyThreadState_Ensure()"); + } + if (ensured->prior_tstate != NULL) { + assert(ensured->was_daemon == 0); + PyThreadState_Clear(tstate); + PyThreadState_Swap(ensured->prior_tstate); + PyMem_RawFree(ensured); + PyThreadState_Delete(tstate); + return; + } + + decrement_daemon_count(tstate->interp); + tstate->ensured = ensured->next; + tstate->daemon = ensured->was_daemon; + PyMem_RawFree(ensured); } From 0ebdca45b386f1adacdf3419665edbc6db0d315c Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 25 Apr 2025 13:27:25 +0000 Subject: [PATCH 07/61] Change some comments. --- Python/pylifecycle.c | 1 + Python/pystate.c | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 4a62bb92b94237..5d4a5cf576cc4b 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -3472,6 +3472,7 @@ wait_for_native_shutdown(PyInterpreterState *interp) struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; PyMutex_Lock(&finalizing->mutex); if (finalizing->countdown == 0) { + // Nothing to do. PyMutex_Unlock(&finalizing->mutex); return; } diff --git a/Python/pystate.c b/Python/pystate.c index 80a84dca1d69cd..57c9c85263b376 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3292,8 +3292,9 @@ PyThreadState_Ensure(PyInterpreterState *interp) } PyThreadState *save = _PyThreadState_GET(); if (save != NULL && save->interp == interp) { + /* We already have a thread state that matches the + interpreter. */ Py_ssize_t refcnt = decref_interpreter(interp); - assert(refcnt > 1); entry->was_daemon = save->daemon; entry->next = save->ensured; entry->prior_tstate = NULL; From 40989de0553cda4882acc9efa4cd603767b48233 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 25 Apr 2025 13:36:47 +0000 Subject: [PATCH 08/61] Add a test that I'm sure doesn't work. --- Programs/_testembed.c | 57 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 57717605d5c256..53077e033a17d7 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2341,6 +2341,15 @@ test_get_incomplete_frame(void) return result; } +const char *THREAD_CODE = "import time\n" + "time.sleep(0.2)\n" + "def fib(n):\n" + " if n <= 1:\n" + " return n\n" + " else:\n" + " return fib(n - 1) + fib(n - 2)\n" + "fib(10)"; + static void non_daemon_native(void *arg) { @@ -2350,15 +2359,7 @@ non_daemon_native(void *arg) int res = PyThreadState_SetDaemon(0); assert(res == 1); _PyEvent_Notify(event); - const char *code = "import time\n" - "time.sleep(0.2)\n" - "def fib(n):\n" - " if n <= 1:\n" - " return n\n" - " else:\n" - " return fib(n - 1) + fib(n - 2)\n" - "fib(10)"; - res = PyRun_SimpleString(code); + res = PyRun_SimpleString(THREAD_CODE); assert(res == 0); PyThreadState_Clear(tstate); PyThreadState_Swap(NULL); @@ -2371,7 +2372,7 @@ test_non_daemon_native_thread(void) _testembed_Py_InitializeFromConfig(); PyThread_handle_t handle; PyThread_ident_t ident; - PyEvent event; + PyEvent event = {0}; if (PyThread_start_joinable_thread(non_daemon_native, &event, &ident, &handle) < 0) { return -1; @@ -2381,6 +2382,41 @@ test_non_daemon_native_thread(void) return 0; } +typedef struct { + PyInterpreterState *interp; + PyEvent *event; +} ThreadData; + +static void +do_tstate_ensure(void *arg) +{ + ThreadData *data = (ThreadData *)arg; + PyEvent *event = data->event; + int res = PyThreadState_Ensure(data->interp); + assert(res == 0); + _PyEvent_Notify(event); + res = PyRun_SimpleString(THREAD_CODE); + assert(res == 0); + PyThreadState_Release(); +} + +static int +test_thread_state_ensure(void) +{ + _testembed_Py_InitializeFromConfig(); + PyThread_handle_t handle; + PyThread_ident_t ident; + PyEvent event = {0}; + ThreadData data = { PyInterpreterState_Hold(), &event }; + if (PyThread_start_joinable_thread(non_daemon_native, &data, + &ident, &handle) < 0) { + return -1; + } + PyEvent_Wait(&event); + Py_Finalize(); + return 0; +} + /* ********************************************************* * List of test cases and the function that implements it. * @@ -2471,6 +2507,7 @@ static struct TestCase TestCases[] = { #endif {"test_get_incomplete_frame", test_get_incomplete_frame}, {"test_non_daemon_native_thread", test_non_daemon_native_thread}, + {"test_thread_state_ensure", test_thread_state_ensure}, {NULL, NULL} }; From d501f35fdea36a6b3ce41b88fee3fad959772357 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 25 Apr 2025 19:44:05 -0400 Subject: [PATCH 09/61] Use the interpreter's reference count and native thread countdown as one. --- Include/internal/pycore_interp_structs.h | 4 -- Programs/_testembed.c | 46 +++--------------- Python/pylifecycle.c | 4 +- Python/pystate.c | 59 +++++++++--------------- 4 files changed, 29 insertions(+), 84 deletions(-) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 07d4a02a76985c..33caf1edfa82c0 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -955,10 +955,6 @@ struct _is { # endif #endif - /* This prevents the interpreter from deleting. - See PEP 788 for the full specification. */ - Py_ssize_t refcount; - /* the initial PyInterpreterState.threads.head */ _PyThreadStateImpl _initial_thread; // _initial_thread should be the last field of PyInterpreterState. diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 53077e033a17d7..8fd8f7ecb1c99b 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2350,54 +2350,21 @@ const char *THREAD_CODE = "import time\n" " return fib(n - 1) + fib(n - 2)\n" "fib(10)"; -static void -non_daemon_native(void *arg) -{ - PyEvent *event = (PyEvent *)arg; - PyThreadState *tstate = PyThreadState_New(PyInterpreterState_Main()); - PyThreadState_Swap(tstate); - int res = PyThreadState_SetDaemon(0); - assert(res == 1); - _PyEvent_Notify(event); - res = PyRun_SimpleString(THREAD_CODE); - assert(res == 0); - PyThreadState_Clear(tstate); - PyThreadState_Swap(NULL); - PyThreadState_Delete(tstate); -} - -static int -test_non_daemon_native_thread(void) -{ - _testembed_Py_InitializeFromConfig(); - PyThread_handle_t handle; - PyThread_ident_t ident; - PyEvent event = {0}; - if (PyThread_start_joinable_thread(non_daemon_native, &event, - &ident, &handle) < 0) { - return -1; - } - PyEvent_Wait(&event); - Py_Finalize(); - return 0; -} - typedef struct { PyInterpreterState *interp; - PyEvent *event; + int done; } ThreadData; static void do_tstate_ensure(void *arg) { ThreadData *data = (ThreadData *)arg; - PyEvent *event = data->event; int res = PyThreadState_Ensure(data->interp); assert(res == 0); - _PyEvent_Notify(event); res = PyRun_SimpleString(THREAD_CODE); assert(res == 0); PyThreadState_Release(); + data->done = 1; } static int @@ -2406,14 +2373,14 @@ test_thread_state_ensure(void) _testembed_Py_InitializeFromConfig(); PyThread_handle_t handle; PyThread_ident_t ident; - PyEvent event = {0}; - ThreadData data = { PyInterpreterState_Hold(), &event }; - if (PyThread_start_joinable_thread(non_daemon_native, &data, + ThreadData data = { PyInterpreterState_Hold() }; + if (PyThread_start_joinable_thread(do_tstate_ensure, &data, &ident, &handle) < 0) { + PyInterpreterState_Release(data.interp); return -1; } - PyEvent_Wait(&event); Py_Finalize(); + assert(data.done == 1); return 0; } @@ -2506,7 +2473,6 @@ static struct TestCase TestCases[] = { {"test_frozenmain", test_frozenmain}, #endif {"test_get_incomplete_frame", test_get_incomplete_frame}, - {"test_non_daemon_native_thread", test_non_daemon_native_thread}, {"test_thread_state_ensure", test_thread_state_ensure}, {NULL, NULL} diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 5d4a5cf576cc4b..a9ce30be38453b 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -2293,7 +2293,6 @@ new_interpreter(PyThreadState **tstate_p, } _PyInterpreterState_SetWhence(interp, whence); interp->_ready = 1; - interp->refcount = 1; // XXX Might new_interpreter() have been called without the GIL held? PyThreadState *save_tstate = _PyThreadState_GET(); @@ -3471,7 +3470,7 @@ wait_for_native_shutdown(PyInterpreterState *interp) assert(interp != NULL); struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; PyMutex_Lock(&finalizing->mutex); - if (finalizing->countdown == 0) { + if (_Py_atomic_load_ssize_relaxed(&finalizing->countdown) == 0) { // Nothing to do. PyMutex_Unlock(&finalizing->mutex); return; @@ -3479,7 +3478,6 @@ wait_for_native_shutdown(PyInterpreterState *interp) PyMutex_Unlock(&finalizing->mutex); PyEvent_Wait(&finalizing->finished); - assert(finalizing->countdown == 0); } int Py_AtExit(void (*func)(void)) diff --git a/Python/pystate.c b/Python/pystate.c index 57c9c85263b376..161bc49a793d5d 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -600,18 +600,6 @@ free_interpreter(PyInterpreterState *interp) } } -static Py_ssize_t -decref_interpreter(PyInterpreterState *interp) -{ - assert(interp != NULL); - Py_ssize_t old_refcnt = _Py_atomic_add_ssize(&interp->refcount, -1); - if (old_refcnt == 1) { - free_interpreter(interp); - } - - return old_refcnt; -} - #ifndef NDEBUG static inline int check_interpreter_whence(long); #endif @@ -805,7 +793,6 @@ _PyInterpreterState_New(PyThreadState *tstate, PyInterpreterState **pinterp) HEAD_UNLOCK(runtime); if (interp != NULL) { - assert(interp->refcount == 1); free_interpreter(interp); } return status; @@ -1056,7 +1043,7 @@ PyInterpreterState_Delete(PyInterpreterState *interp) _PyObject_FiniState(interp); - decref_interpreter(interp); + free_interpreter(interp); } @@ -1092,7 +1079,7 @@ _PyInterpreterState_DeleteExceptMain(_PyRuntimeState *runtime) zapthreads(interp); PyInterpreterState *prev_interp = interp; interp = interp->next; - decref_interpreter(prev_interp); + free_interpreter(prev_interp); } HEAD_UNLOCK(runtime); @@ -1825,7 +1812,7 @@ decrement_daemon_count(PyInterpreterState *interp) { assert(interp != NULL); struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; - if (--finalizing->countdown == 0) { + if (_Py_atomic_load_ssize_relaxed(&finalizing->countdown) == 1) { _PyEvent_Notify(&finalizing->finished); } } @@ -3221,8 +3208,7 @@ Py_ssize_t _PyInterpreterState_Refcount(PyInterpreterState *interp) { assert(interp != NULL); - Py_ssize_t refcount = _Py_atomic_load_ssize_relaxed(&interp->refcount); - assert(refcount > 0); + Py_ssize_t refcount = _Py_atomic_load_ssize_relaxed(&interp->threads.finalizing.countdown); return refcount; } @@ -3230,8 +3216,8 @@ PyInterpreterState * PyInterpreterState_Hold(void) { PyInterpreterState *interp = PyInterpreterState_Get(); - assert(_Py_atomic_load_ssize_relaxed(&interp->refcount) > 0); - _Py_atomic_add_ssize(&interp->refcount, 1); + assert(_Py_atomic_load_ssize_relaxed(&interp->threads.finalizing.countdown) >= 0); + _Py_atomic_add_ssize(&interp->threads.finalizing.countdown, 1); return interp; } @@ -3239,7 +3225,7 @@ void PyInterpreterState_Release(PyInterpreterState *interp) { assert(interp != NULL); - decref_interpreter(interp); + decrement_daemon_count(interp); } static int @@ -3251,12 +3237,16 @@ tstate_set_daemon(PyThreadState *tstate, PyInterpreterState *interp, int daemon) assert(daemon == 1 || daemon == 0); struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; PyMutex_Lock(&finalizing->mutex); + if (_Py_atomic_load_ssize_relaxed(&finalizing->countdown) == 0) { + PyMutex_Unlock(&finalizing->mutex); + return -1; + } if (_PyEvent_IsSet(&finalizing->finished)) { /* Native threads have already finalized */ PyMutex_Unlock(&finalizing->mutex); return -1; } - ++finalizing->countdown; + _Py_atomic_add_ssize(&finalizing->countdown, 1); PyMutex_Unlock(&finalizing->mutex); tstate->daemon = daemon; return 1; @@ -3271,7 +3261,7 @@ PyThreadState_SetDaemon(int daemon) } PyInterpreterState *interp = tstate->interp; assert(interp != NULL); - if (tstate == &interp->_initial_thread) { + if (tstate == (PyThreadState *)&interp->_initial_thread) { Py_FatalError("thread cannot be the main thread"); } if (tstate->daemon == daemon) { @@ -3287,39 +3277,31 @@ PyThreadState_Ensure(PyInterpreterState *interp) assert(interp != NULL); _Py_ensured_tstate *entry = PyMem_RawMalloc(sizeof(_Py_ensured_tstate)); if (entry == NULL) { - decref_interpreter(interp); + decrement_daemon_count(interp); return -1; } PyThreadState *save = _PyThreadState_GET(); if (save != NULL && save->interp == interp) { - /* We already have a thread state that matches the - interpreter. */ - Py_ssize_t refcnt = decref_interpreter(interp); entry->was_daemon = save->daemon; entry->next = save->ensured; entry->prior_tstate = NULL; - if (tstate_set_daemon(save, interp, 0) < 0) { - PyMem_RawFree(entry); - return -1; - } save->ensured = entry; + // Setting 'daemon' to 0 passes off the interpreter's reference + save->daemon = 0; return 0; } PyThreadState *tstate = PyThreadState_New(interp); - decref_interpreter(interp); if (tstate == NULL) { PyMem_RawFree(entry); return -1; } - if (tstate_set_daemon(tstate, interp, 0) < 0) { - PyMem_RawFree(entry); - return -1; - } + tstate->daemon = 0; entry->was_daemon = 0; entry->prior_tstate = save; entry->next = NULL; tstate->ensured = entry; + PyThreadState_Swap(tstate); return 0; } @@ -3342,8 +3324,11 @@ PyThreadState_Release(void) return; } - decrement_daemon_count(tstate->interp); tstate->ensured = ensured->next; tstate->daemon = ensured->was_daemon; PyMem_RawFree(ensured); + PyThreadState_Clear(tstate); + PyThreadState_Swap(NULL); + PyInterpreterState *interp = tstate->interp; + PyThreadState_Delete(tstate); } From c5ec89ca8759cd3edf82e335706218116a6e0236 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 25 Apr 2025 20:09:07 -0400 Subject: [PATCH 10/61] Fix the countdown decrement. --- Programs/_testembed.c | 6 ++++++ Python/pystate.c | 14 +++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 8fd8f7ecb1c99b..a65238138ec949 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2361,7 +2361,13 @@ do_tstate_ensure(void *arg) ThreadData *data = (ThreadData *)arg; int res = PyThreadState_Ensure(data->interp); assert(res == 0); + PyThreadState_Ensure(PyInterpreterState_Hold()); + PyThreadState_Ensure(PyInterpreterState_Hold()); + PyThreadState_Ensure(PyInterpreterState_Hold()); res = PyRun_SimpleString(THREAD_CODE); + PyThreadState_Release(); + PyThreadState_Release(); + PyThreadState_Release(); assert(res == 0); PyThreadState_Release(); data->done = 1; diff --git a/Python/pystate.c b/Python/pystate.c index 161bc49a793d5d..8e5277fa6cb981 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1812,7 +1812,7 @@ decrement_daemon_count(PyInterpreterState *interp) { assert(interp != NULL); struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; - if (_Py_atomic_load_ssize_relaxed(&finalizing->countdown) == 1) { + if (_Py_atomic_add_ssize(&finalizing->countdown, -1) == 1) { _PyEvent_Notify(&finalizing->finished); } } @@ -3324,11 +3324,15 @@ PyThreadState_Release(void) return; } + PyInterpreterState *interp = tstate->interp; tstate->ensured = ensured->next; tstate->daemon = ensured->was_daemon; PyMem_RawFree(ensured); - PyThreadState_Clear(tstate); - PyThreadState_Swap(NULL); - PyInterpreterState *interp = tstate->interp; - PyThreadState_Delete(tstate); + if (tstate->ensured == NULL) { + PyThreadState_Clear(tstate); + PyThreadState_Swap(NULL); + PyThreadState_Delete(tstate); + } else { + decrement_daemon_count(tstate->interp); + } } From 4e1f599ca19d68120773dd4d50889e7a21490c06 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 25 Apr 2025 20:12:01 -0400 Subject: [PATCH 11/61] Remove unused variable. --- Python/pystate.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Python/pystate.c b/Python/pystate.c index 8e5277fa6cb981..90a2a27da65fd3 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3324,7 +3324,6 @@ PyThreadState_Release(void) return; } - PyInterpreterState *interp = tstate->interp; tstate->ensured = ensured->next; tstate->daemon = ensured->was_daemon; PyMem_RawFree(ensured); From 3127a3fe2782e6c26cfdf33b459f357006091fb7 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 25 Apr 2025 20:16:28 -0400 Subject: [PATCH 12/61] Test for PyGILState_Ensure() --- Programs/_testembed.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index a65238138ec949..a9effc43735064 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2363,9 +2363,11 @@ do_tstate_ensure(void *arg) assert(res == 0); PyThreadState_Ensure(PyInterpreterState_Hold()); PyThreadState_Ensure(PyInterpreterState_Hold()); + PyGILState_STATE gstate = PyGILState_Ensure(); PyThreadState_Ensure(PyInterpreterState_Hold()); res = PyRun_SimpleString(THREAD_CODE); PyThreadState_Release(); + PyGILState_Release(gstate); PyThreadState_Release(); PyThreadState_Release(); assert(res == 0); From 82b5b9f4d937bb080eb0deb68fc4983a66e755cf Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 25 Apr 2025 20:49:12 -0400 Subject: [PATCH 13/61] Fix the test for the new reference counting. --- Modules/_testcapimodule.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index bde04705aa6826..2e7a432e478599 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2555,17 +2555,18 @@ test_interp_refcount(PyObject *self, PyObject *unused) PyInterpreterState *interp = PyThreadState_GetInterpreter(tstate); assert(interp != NULL); - assert(_PyInterpreterState_Refcount(interp) == 1); + assert(_PyInterpreterState_Refcount(interp) == 0); PyInterpreterState *held = PyInterpreterState_Hold(); - assert(_PyInterpreterState_Refcount(interp) == 2); - PyInterpreterState_Release(held); assert(_PyInterpreterState_Refcount(interp) == 1); + PyInterpreterState_Release(held); + assert(_PyInterpreterState_Refcount(interp) == 0); held = PyInterpreterState_Hold(); Py_EndInterpreter(tstate); PyThreadState_Swap(save); assert(_PyInterpreterState_Refcount(interp) == 1); PyInterpreterState_Release(held); + assert(_PyInterpreterState_Refcount(interp) == 0); Py_RETURN_NONE; } From fda9886edd26ed7a7e1946f5030cfe96bcbbe2a6 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 26 Apr 2025 11:52:29 -0400 Subject: [PATCH 14/61] Add PyInterpreterState_Lookup() --- Include/cpython/pystate.h | 2 ++ Python/pystate.c | 26 +++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index f20ad0fd507bf9..9de35fd0ac807a 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -283,6 +283,8 @@ PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameFunc( * PyThreadState_Ensure() or PyInterpreterState_Release(). */ PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Hold(void); +PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Lookup(int64_t interp_id); + /* Release a reference to an interpreter incremented by PyInterpreterState_Hold() */ PyAPI_FUNC(void) PyInterpreterState_Release(PyInterpreterState *interp); diff --git a/Python/pystate.c b/Python/pystate.c index 90a2a27da65fd3..0d566abee603af 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3074,9 +3074,6 @@ _PyThreadState_CheckConsistency(PyThreadState *tstate) int _PyThreadState_MustExit(PyThreadState *tstate) { - if (!tstate->daemon) { - return 0; - } int state = _Py_atomic_load_int_relaxed(&tstate->state); return state == _Py_THREAD_SHUTTING_DOWN; } @@ -3221,6 +3218,29 @@ PyInterpreterState_Hold(void) return interp; } +PyInterpreterState * +PyInterpreterState_Lookup(int64_t interp_id) +{ + PyInterpreterState *interp = _PyInterpreterState_LookUpID(interp_id); + if (interp == NULL) { + return NULL; + } + HEAD_LOCK(&_PyRuntime); // Prevent deletion + struct _Py_finalizing_threads finalizing = interp->threads.finalizing; + PyMutex *mutex = &finalizing.mutex; + PyMutex_Lock(mutex); // Synchronize TOCTOU with the event flag + if (_PyEvent_IsSet(&finalizing.finished)) { + /* Interpreter has already finished threads */ + interp = NULL; + } else { + _Py_atomic_add_ssize(&finalizing.countdown, 1); + } + PyMutex_Unlock(mutex); + HEAD_UNLOCK(&_PyRuntime); + + return interp; +} + void PyInterpreterState_Release(PyInterpreterState *interp) { From f7723c0aa18847906984e8f52b70e906708196b3 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 26 Apr 2025 12:13:13 -0400 Subject: [PATCH 15/61] Fix a few bugs and add a test. --- Include/internal/pycore_interp_structs.h | 1 + Modules/_testcapimodule.c | 21 ++++++++++++ Python/pylifecycle.c | 1 + Python/pystate.c | 43 ++++++++++++++++++------ 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 33caf1edfa82c0..190bee6a614f12 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -797,6 +797,7 @@ struct _is { Py_ssize_t countdown; PyEvent finished; PyMutex mutex; + int shutting_down; } finalizing; } threads; diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 2e7a432e478599..e043dfed7c8112 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2571,6 +2571,26 @@ test_interp_refcount(PyObject *self, PyObject *unused) Py_RETURN_NONE; } +static PyObject * +test_interp_lookup(PyObject *self, PyObject *unused) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + assert(_PyInterpreterState_Refcount(interp) == 0); + int64_t interp_id = PyInterpreterState_GetID(interp); + PyInterpreterState *ref = PyInterpreterState_Lookup(interp_id); + assert(ref == interp); + assert(_PyInterpreterState_Refcount(interp) == 1); + PyInterpreterState_Release(ref); + assert(PyInterpreterState_Lookup(10000) == NULL); + Py_BEGIN_ALLOW_THREADS; + ref = PyInterpreterState_Lookup(interp_id); + assert(ref == interp); + PyInterpreterState_Release(ref); + Py_END_ALLOW_THREADS; + + Py_RETURN_NONE; +} + static PyMethodDef TestMethods[] = { {"set_errno", set_errno, METH_VARARGS}, {"test_config", test_config, METH_NOARGS}, @@ -2666,6 +2686,7 @@ static PyMethodDef TestMethods[] = { {"code_offset_to_line", _PyCFunction_CAST(code_offset_to_line), METH_FASTCALL}, {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, {"test_interp_refcount", test_interp_refcount, METH_NOARGS}, + {"test_interp_lookup", test_interp_lookup, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index a9ce30be38453b..cd89ca2c3386d2 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -3469,6 +3469,7 @@ wait_for_native_shutdown(PyInterpreterState *interp) { assert(interp != NULL); struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; + _Py_atomic_store_int_release(&finalizing->shutting_down, 1); PyMutex_Lock(&finalizing->mutex); if (_Py_atomic_load_ssize_relaxed(&finalizing->countdown) == 0) { // Nothing to do. diff --git a/Python/pystate.c b/Python/pystate.c index 0d566abee603af..862fb6a488f282 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1373,12 +1373,8 @@ interp_look_up_id(_PyRuntimeState *runtime, int64_t requested_id) return NULL; } -/* Return the interpreter state with the given ID. - - Fail with RuntimeError if the interpreter is not found. */ - PyInterpreterState * -_PyInterpreterState_LookUpID(int64_t requested_id) +_PyInterpreterState_LookUpIDNoErr(int64_t requested_id) { PyInterpreterState *interp = NULL; if (requested_id >= 0) { @@ -1387,6 +1383,18 @@ _PyInterpreterState_LookUpID(int64_t requested_id) interp = interp_look_up_id(runtime, requested_id); HEAD_UNLOCK(runtime); } + return interp; +} + +/* Return the interpreter state with the given ID. + + Fail with RuntimeError if the interpreter is not found. */ + +PyInterpreterState * +_PyInterpreterState_LookUpID(int64_t requested_id) +{ + assert(_PyThreadState_GET() != NULL); + PyInterpreterState *interp = _PyInterpreterState_LookUpIDNoErr(requested_id); if (interp == NULL && !PyErr_Occurred()) { PyErr_Format(PyExc_InterpreterNotFoundError, "unrecognized interpreter ID %lld", requested_id); @@ -1807,13 +1815,23 @@ PyThreadState_Clear(PyThreadState *tstate) static void decrement_stoptheworld_countdown(struct _stoptheworld_state *stw); +static int +shutting_down_natives(PyInterpreterState *interp) +{ + assert(interp != NULL); + return _Py_atomic_load_int_relaxed(&interp->threads.finalizing.shutting_down); +} + static void decrement_daemon_count(PyInterpreterState *interp) { assert(interp != NULL); struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; - if (_Py_atomic_add_ssize(&finalizing->countdown, -1) == 1) { + Py_ssize_t old = _Py_atomic_add_ssize(&finalizing->countdown, -1); + if (old == 1 && shutting_down_natives(interp)) { _PyEvent_Notify(&finalizing->finished); + } else if (old <= 0) { + Py_FatalError("interpreter has negative reference count"); } } @@ -3074,6 +3092,9 @@ _PyThreadState_CheckConsistency(PyThreadState *tstate) int _PyThreadState_MustExit(PyThreadState *tstate) { + if (!tstate->daemon) { + return 0; + } int state = _Py_atomic_load_int_relaxed(&tstate->state); return state == _Py_THREAD_SHUTTING_DOWN; } @@ -3221,19 +3242,19 @@ PyInterpreterState_Hold(void) PyInterpreterState * PyInterpreterState_Lookup(int64_t interp_id) { - PyInterpreterState *interp = _PyInterpreterState_LookUpID(interp_id); + PyInterpreterState *interp = _PyInterpreterState_LookUpIDNoErr(interp_id); if (interp == NULL) { return NULL; } HEAD_LOCK(&_PyRuntime); // Prevent deletion - struct _Py_finalizing_threads finalizing = interp->threads.finalizing; - PyMutex *mutex = &finalizing.mutex; + struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; + PyMutex *mutex = &finalizing->mutex; PyMutex_Lock(mutex); // Synchronize TOCTOU with the event flag - if (_PyEvent_IsSet(&finalizing.finished)) { + if (_PyEvent_IsSet(&finalizing->finished)) { /* Interpreter has already finished threads */ interp = NULL; } else { - _Py_atomic_add_ssize(&finalizing.countdown, 1); + _Py_atomic_add_ssize(&finalizing->countdown, 1); } PyMutex_Unlock(mutex); HEAD_UNLOCK(&_PyRuntime); From 62e95491065f2a37aa8827641c90c6a4a88c0466 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 27 Apr 2025 10:36:45 -0400 Subject: [PATCH 16/61] Add a test for PyThreadState_Ensure() across interpreters. --- Include/cpython/pystate.h | 3 ++- Modules/_testcapimodule.c | 31 +++++++++++++++++++++++++++++++ Python/pystate.c | 11 +++++++++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 9de35fd0ac807a..a72eb038328aa3 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -288,8 +288,9 @@ PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Lookup(int64_t interp_id); /* Release a reference to an interpreter incremented by PyInterpreterState_Hold() */ PyAPI_FUNC(void) PyInterpreterState_Release(PyInterpreterState *interp); -// Export for '_testcapi' shared extension +// Exports for '_testcapi' shared extension PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); +PyAPI_FUNC(void) _PyInterpreterState_Incref(PyInterpreterState *interp); PyAPI_FUNC(int) PyThreadState_SetDaemon(int daemon); diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index e043dfed7c8112..3d71445cef5138 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2591,6 +2591,36 @@ test_interp_lookup(PyObject *self, PyObject *unused) Py_RETURN_NONE; } +static PyObject * +test_interp_ensure(PyObject *self, PyObject *unused) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + PyThreadState *save_tstate = PyThreadState_Swap(NULL); + PyThreadState *tstate = Py_NewInterpreter(); + PyInterpreterState *subinterp = PyThreadState_GetInterpreter(tstate); + + for (int i = 0; i < 10; ++i) { + _PyInterpreterState_Incref(interp); + int res = PyThreadState_Ensure(interp); + assert(res == 0); + assert(PyInterpreterState_Get() == interp); + } + + for (int i = 0; i < 10; ++i) { + _PyInterpreterState_Incref(subinterp); + int res = PyThreadState_Ensure(subinterp); + assert(res == 0); + assert(PyInterpreterState_Get() == subinterp); + } + + for (int i = 0; i < 20; ++i) { + PyThreadState_Release(); + } + + PyThreadState_Swap(save_tstate); + Py_RETURN_NONE; +} + static PyMethodDef TestMethods[] = { {"set_errno", set_errno, METH_VARARGS}, {"test_config", test_config, METH_NOARGS}, @@ -2687,6 +2717,7 @@ static PyMethodDef TestMethods[] = { {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, {"test_interp_refcount", test_interp_refcount, METH_NOARGS}, {"test_interp_lookup", test_interp_lookup, METH_NOARGS}, + {"test_interp_ensure", test_interp_ensure, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/Python/pystate.c b/Python/pystate.c index 862fb6a488f282..cd7e7918edb5c6 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3230,12 +3230,19 @@ _PyInterpreterState_Refcount(PyInterpreterState *interp) return refcount; } +void +_PyInterpreterState_Incref(PyInterpreterState *interp) +{ + assert(interp != NULL); + assert(_Py_atomic_load_ssize_relaxed(&interp->threads.finalizing.countdown) >= 0); + _Py_atomic_add_ssize(&interp->threads.finalizing.countdown, 1); +} + PyInterpreterState * PyInterpreterState_Hold(void) { PyInterpreterState *interp = PyInterpreterState_Get(); - assert(_Py_atomic_load_ssize_relaxed(&interp->threads.finalizing.countdown) >= 0); - _Py_atomic_add_ssize(&interp->threads.finalizing.countdown, 1); + _PyInterpreterState_Incref(interp); return interp; } From bc6063076e12c46920645253001e9f178414d72f Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 28 Apr 2025 16:12:50 -0400 Subject: [PATCH 17/61] Remove an artifact from old approach. --- Python/pystate.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Python/pystate.c b/Python/pystate.c index cd7e7918edb5c6..084e9f4afe5c85 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -2152,7 +2152,7 @@ tstate_wait_attach(PyThreadState *tstate) _PyParkingLot_Park(&tstate->state, &state, sizeof(tstate->state), /*timeout=*/-1, NULL, /*detach=*/0); } - else if (state == _Py_THREAD_SHUTTING_DOWN && tstate->daemon) { + else if (state == _Py_THREAD_SHUTTING_DOWN) { // We're shutting down, so we can't attach. _PyThreadState_HangThread(tstate); } @@ -3092,9 +3092,6 @@ _PyThreadState_CheckConsistency(PyThreadState *tstate) int _PyThreadState_MustExit(PyThreadState *tstate) { - if (!tstate->daemon) { - return 0; - } int state = _Py_atomic_load_int_relaxed(&tstate->state); return state == _Py_THREAD_SHUTTING_DOWN; } From 9d8d526274978303f3c04cce5c3b1db49ea49baa Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 28 Apr 2025 16:43:12 -0400 Subject: [PATCH 18/61] Fix test from earlier semantics. --- Modules/_testcapimodule.c | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 3d71445cef5138..06243ba225366e 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2549,21 +2549,15 @@ toggle_reftrace_printer(PyObject *ob, PyObject *arg) static PyObject * test_interp_refcount(PyObject *self, PyObject *unused) { - PyThreadState *save = PyThreadState_Get(); - PyThreadState *tstate = Py_NewInterpreter(); - assert(tstate == PyThreadState_Get()); - PyInterpreterState *interp = PyThreadState_GetInterpreter(tstate); - assert(interp != NULL); + PyInterpreterState *interp = PyInterpreterState_Get(); + // Reference counts are technically 0 by default assert(_PyInterpreterState_Refcount(interp) == 0); PyInterpreterState *held = PyInterpreterState_Hold(); assert(_PyInterpreterState_Refcount(interp) == 1); - PyInterpreterState_Release(held); - assert(_PyInterpreterState_Refcount(interp) == 0); - held = PyInterpreterState_Hold(); - Py_EndInterpreter(tstate); - PyThreadState_Swap(save); + assert(_PyInterpreterState_Refcount(interp) == 2); + PyInterpreterState_Release(held); assert(_PyInterpreterState_Refcount(interp) == 1); PyInterpreterState_Release(held); assert(_PyInterpreterState_Refcount(interp) == 0); From 54b0ce027eade2b2a5f9c74ddf4eb2a032feb224 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 22 May 2025 15:45:24 -0400 Subject: [PATCH 19/61] Remove 'daemonness' as a property of a thread. --- Python/pylifecycle.c | 4 +- Python/pystate.c | 108 ++----------------------------------------- 2 files changed, 5 insertions(+), 107 deletions(-) diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index cd89ca2c3386d2..c31509f74f3dc1 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -2014,7 +2014,7 @@ _Py_Finalize(_PyRuntimeState *runtime) // Wrap up existing "threading"-module-created, non-daemon threads. wait_for_thread_shutdown(tstate); - // Wrap up non-daemon native threads + // Wait for the interpreter's reference count to reach zero wait_for_native_shutdown(tstate->interp); // Make any remaining pending calls. @@ -2433,7 +2433,7 @@ Py_EndInterpreter(PyThreadState *tstate) // Wrap up existing "threading"-module-created, non-daemon threads. wait_for_thread_shutdown(tstate); - // Wrap up non-daemon native threads + // Wait for the interpreter's reference count to reach zero wait_for_native_shutdown(tstate->interp); // Make any remaining pending calls. diff --git a/Python/pystate.c b/Python/pystate.c index 084e9f4afe5c85..2faebbc2f284c5 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1610,8 +1610,6 @@ new_threadstate(PyInterpreterState *interp, int whence) return NULL; } #endif - ((PyThreadState *)tstate)->daemon = 1; - /* We serialize concurrent creation to protect global state. */ HEAD_LOCK(interp->runtime); @@ -1823,7 +1821,7 @@ shutting_down_natives(PyInterpreterState *interp) } static void -decrement_daemon_count(PyInterpreterState *interp) +decref_interpreter(PyInterpreterState *interp) { assert(interp != NULL); struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; @@ -1869,10 +1867,6 @@ tstate_delete_common(PyThreadState *tstate, int release_gil) decrement_stoptheworld_countdown(&runtime->stoptheworld); } } - if (tstate->daemon == 0 - && tstate != (PyThreadState *)&interp->_initial_thread) { - decrement_daemon_count(interp); - } #if defined(Py_REF_DEBUG) && defined(Py_GIL_DISABLED) // Add our portion of the total refcount to the interpreter's total. @@ -3270,113 +3264,17 @@ void PyInterpreterState_Release(PyInterpreterState *interp) { assert(interp != NULL); - decrement_daemon_count(interp); -} - -static int -tstate_set_daemon(PyThreadState *tstate, PyInterpreterState *interp, int daemon) -{ - assert(tstate != NULL); - assert(interp != NULL); - assert(tstate->interp == interp); - assert(daemon == 1 || daemon == 0); - struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; - PyMutex_Lock(&finalizing->mutex); - if (_Py_atomic_load_ssize_relaxed(&finalizing->countdown) == 0) { - PyMutex_Unlock(&finalizing->mutex); - return -1; - } - if (_PyEvent_IsSet(&finalizing->finished)) { - /* Native threads have already finalized */ - PyMutex_Unlock(&finalizing->mutex); - return -1; - } - _Py_atomic_add_ssize(&finalizing->countdown, 1); - PyMutex_Unlock(&finalizing->mutex); - tstate->daemon = daemon; - return 1; -} - -int -PyThreadState_SetDaemon(int daemon) -{ - PyThreadState *tstate = PyThreadState_Get(); - if (daemon != 0 && daemon != 1) { - Py_FatalError("daemon must be 0 or 1"); - } - PyInterpreterState *interp = tstate->interp; - assert(interp != NULL); - if (tstate == (PyThreadState *)&interp->_initial_thread) { - Py_FatalError("thread cannot be the main thread"); - } - if (tstate->daemon == daemon) { - return 0; - } - - return tstate_set_daemon(tstate, interp, daemon); + decref_interpreter(interp); } int PyThreadState_Ensure(PyInterpreterState *interp) { - assert(interp != NULL); - _Py_ensured_tstate *entry = PyMem_RawMalloc(sizeof(_Py_ensured_tstate)); - if (entry == NULL) { - decrement_daemon_count(interp); - return -1; - } - PyThreadState *save = _PyThreadState_GET(); - if (save != NULL && save->interp == interp) { - entry->was_daemon = save->daemon; - entry->next = save->ensured; - entry->prior_tstate = NULL; - save->ensured = entry; - // Setting 'daemon' to 0 passes off the interpreter's reference - save->daemon = 0; - return 0; - } - - PyThreadState *tstate = PyThreadState_New(interp); - if (tstate == NULL) { - PyMem_RawFree(entry); - return -1; - } - tstate->daemon = 0; - entry->was_daemon = 0; - entry->prior_tstate = save; - entry->next = NULL; - tstate->ensured = entry; - PyThreadState_Swap(tstate); - return 0; } void PyThreadState_Release(void) { - PyThreadState *tstate = _PyThreadState_GET(); - _Py_EnsureTstateNotNULL(tstate); - _Py_ensured_tstate *ensured = tstate->ensured; - if (ensured == NULL) { - Py_FatalError("PyThreadState_Release() called without PyThreadState_Ensure()"); - } - if (ensured->prior_tstate != NULL) { - assert(ensured->was_daemon == 0); - PyThreadState_Clear(tstate); - PyThreadState_Swap(ensured->prior_tstate); - PyMem_RawFree(ensured); - PyThreadState_Delete(tstate); - return; - } - - tstate->ensured = ensured->next; - tstate->daemon = ensured->was_daemon; - PyMem_RawFree(ensured); - if (tstate->ensured == NULL) { - PyThreadState_Clear(tstate); - PyThreadState_Swap(NULL); - PyThreadState_Delete(tstate); - } else { - decrement_daemon_count(tstate->interp); - } + return 0; } From 5955de6df592ed8d683b1ff2435220713d3bda4d Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 22 May 2025 15:58:29 -0400 Subject: [PATCH 20/61] Add strong interpreter reference functions. --- Include/cpython/pystate.h | 33 +++++++++++++++++++++++---------- Python/pystate.c | 39 +++++++++++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index a72eb038328aa3..ab15f73b4d3b59 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -277,23 +277,36 @@ PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameFunc( PyInterpreterState *interp, _PyFrameEvalFunction eval_frame); -/* Similar to PyInterpreterState_Get(), but returns the interpreter with an - * incremented reference count. PyInterpreterState_Delete() won't delete the - * full interpreter structure until the reference is released by - * PyThreadState_Ensure() or PyInterpreterState_Release(). */ -PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Hold(void); +/* Strong interpreter references */ -PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Lookup(int64_t interp_id); +typedef uintptr_t PyInterpreterRef; -/* Release a reference to an interpreter incremented by PyInterpreterState_Hold() */ -PyAPI_FUNC(void) PyInterpreterState_Release(PyInterpreterState *interp); +PyAPI_FUNC(PyInterpreterRef) PyInterpreterRef_Get(void); +PyAPI_FUNC(PyInterpreterRef) PyInterpreterRef_Dup(PyInterpreterRef ref); +PyAPI_FUNC(PyInterpreterRef) PyInterpreterState_AsStrong(PyInterpreterState *interp); +PyAPI_FUNC(void) PyInterpreterRef_Close(PyInterpreterRef ref); + +#define PyInterpreterRef_Close(ref) do { \ + PyInterpreterRef_Close(ref); \ + ref = 0; \ +} while (0); \ + +/* Weak interpreter references */ + +typedef struct _interpreter_weakref { + int64_t id; + Py_ssize_t refcount; +} PyInterpreterWeakRef; + +PyAPI_FUNC(PyInterpreterWeakRef) PyInterpreterWeakRef_Get(void); +PyAPI_FUNC(PyInterpreterWeakRef) PyInterpreterWeakRef_Dup(PyInterpreterWeakRef wref); +PyAPI_FUNC(PyInterpreterRef) PyInterpreterWeakRef_AsStrong(PyInterpreterWeakRef wref); +PyAPI_FUNC(void) PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref); // Exports for '_testcapi' shared extension PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); PyAPI_FUNC(void) _PyInterpreterState_Incref(PyInterpreterState *interp); -PyAPI_FUNC(int) PyThreadState_SetDaemon(int daemon); - PyAPI_FUNC(int) PyThreadState_Ensure(PyInterpreterState *interp); PyAPI_FUNC(void) PyThreadState_Release(void); diff --git a/Python/pystate.c b/Python/pystate.c index 2faebbc2f284c5..4d118423d204dd 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3229,12 +3229,38 @@ _PyInterpreterState_Incref(PyInterpreterState *interp) _Py_atomic_add_ssize(&interp->threads.finalizing.countdown, 1); } -PyInterpreterState * -PyInterpreterState_Hold(void) +static PyInterpreterState * +ref_as_interp(PyInterpreterRef ref) +{ + PyInterpreterState *interp = (PyInterpreterState *)ref; + if (interp == NULL) { + Py_FatalError("Got a null interpreter reference, likely due to use after close."); + } + + return interp; +} + +PyInterpreterRef +PyInterpreterRef_Get(void) { PyInterpreterState *interp = PyInterpreterState_Get(); _PyInterpreterState_Incref(interp); - return interp; + return (PyInterpreterRef)interp; +} + +PyInterpreterRef +PyInterpreterRef_Dup(PyInterpreterRef ref) +{ + PyInterpreterState *interp = ref_as_interp(ref); + _PyInterpreterState_Incref(interp); + return (PyInterpreterRef)interp; +} + +void +PyInterpreterRef_Close(PyInterpreterRef ref) +{ + PyInterpreterState *interp = ref_as_interp(ref); + decref_interpreter(ref); } PyInterpreterState * @@ -3260,13 +3286,6 @@ PyInterpreterState_Lookup(int64_t interp_id) return interp; } -void -PyInterpreterState_Release(PyInterpreterState *interp) -{ - assert(interp != NULL); - decref_interpreter(interp); -} - int PyThreadState_Ensure(PyInterpreterState *interp) { From 92cf90603e7910b7f59f1ab36a677cb17b76aa36 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 22 May 2025 16:05:48 -0400 Subject: [PATCH 21/61] Implement weak references. --- Include/cpython/pystate.h | 2 +- Python/pystate.c | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index ab15f73b4d3b59..8e2327bc0fc027 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -300,7 +300,7 @@ typedef struct _interpreter_weakref { PyAPI_FUNC(PyInterpreterWeakRef) PyInterpreterWeakRef_Get(void); PyAPI_FUNC(PyInterpreterWeakRef) PyInterpreterWeakRef_Dup(PyInterpreterWeakRef wref); -PyAPI_FUNC(PyInterpreterRef) PyInterpreterWeakRef_AsStrong(PyInterpreterWeakRef wref); +PyAPI_FUNC(int) PyInterpreterWeakRef_AsStrong(PyInterpreterWeakRef wref, PyInterpreterRef *strong_ptr); PyAPI_FUNC(void) PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref); // Exports for '_testcapi' shared extension diff --git a/Python/pystate.c b/Python/pystate.c index 4d118423d204dd..72963801d644ef 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3256,6 +3256,7 @@ PyInterpreterRef_Dup(PyInterpreterRef ref) return (PyInterpreterRef)interp; } +#undef PyInterpreterRef_Close void PyInterpreterRef_Close(PyInterpreterRef ref) { @@ -3263,12 +3264,36 @@ PyInterpreterRef_Close(PyInterpreterRef ref) decref_interpreter(ref); } -PyInterpreterState * -PyInterpreterState_Lookup(int64_t interp_id) + +PyInterpreterWeakRef +PyInterpreterWeakRef_Get(void) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + PyInterpreterWeakRef wref = { interp->id }; + return wref; +} + +PyInterpreterWeakRef +PyInterpreterWeakRef_Dup(PyInterpreterWeakRef wref) +{ + return wref; +} + +void +PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref) +{ + return; +} + +int +PyInterpreterWeakRef_AsStrong(PyInterpreterWeakRef wref, PyInterpreterRef *strong_ptr) { + assert(strong_ptr != NULL); + int64_t interp_id = wref.id; PyInterpreterState *interp = _PyInterpreterState_LookUpIDNoErr(interp_id); if (interp == NULL) { - return NULL; + *strong_ptr = 0; + return -1; } HEAD_LOCK(&_PyRuntime); // Prevent deletion struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; @@ -3282,8 +3307,9 @@ PyInterpreterState_Lookup(int64_t interp_id) } PyMutex_Unlock(mutex); HEAD_UNLOCK(&_PyRuntime); + *strong_ptr = (PyInterpreterRef)interp; - return interp; + return 0; } int From b0d0673ef43295f2d183e5366b97b7cdf349b803 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 22 May 2025 16:23:43 -0400 Subject: [PATCH 22/61] Fix some thread safety issues regarding interpreter deletion. --- Include/cpython/pystate.h | 2 +- Python/pystate.c | 55 +++++++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 8e2327bc0fc027..cb400820eade97 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -283,7 +283,7 @@ typedef uintptr_t PyInterpreterRef; PyAPI_FUNC(PyInterpreterRef) PyInterpreterRef_Get(void); PyAPI_FUNC(PyInterpreterRef) PyInterpreterRef_Dup(PyInterpreterRef ref); -PyAPI_FUNC(PyInterpreterRef) PyInterpreterState_AsStrong(PyInterpreterState *interp); +PyAPI_FUNC(int) PyInterpreterState_AsStrong(PyInterpreterState *interp, PyInterpreterRef *strong_ptr); PyAPI_FUNC(void) PyInterpreterRef_Close(PyInterpreterRef ref); #define PyInterpreterRef_Close(ref) do { \ diff --git a/Python/pystate.c b/Python/pystate.c index 72963801d644ef..aa680a77757502 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3261,10 +3261,9 @@ void PyInterpreterRef_Close(PyInterpreterRef ref) { PyInterpreterState *interp = ref_as_interp(ref); - decref_interpreter(ref); + decref_interpreter(interp); } - PyInterpreterWeakRef PyInterpreterWeakRef_Get(void) { @@ -3285,33 +3284,57 @@ PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref) return; } -int -PyInterpreterWeakRef_AsStrong(PyInterpreterWeakRef wref, PyInterpreterRef *strong_ptr) +static int +try_acquire_strong_ref(PyInterpreterState *interp, PyInterpreterRef *strong_ptr) { - assert(strong_ptr != NULL); - int64_t interp_id = wref.id; - PyInterpreterState *interp = _PyInterpreterState_LookUpIDNoErr(interp_id); - if (interp == NULL) { - *strong_ptr = 0; - return -1; - } - HEAD_LOCK(&_PyRuntime); // Prevent deletion struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; PyMutex *mutex = &finalizing->mutex; PyMutex_Lock(mutex); // Synchronize TOCTOU with the event flag if (_PyEvent_IsSet(&finalizing->finished)) { /* Interpreter has already finished threads */ - interp = NULL; + *strong_ptr = 0; + return -1; } else { _Py_atomic_add_ssize(&finalizing->countdown, 1); } PyMutex_Unlock(mutex); - HEAD_UNLOCK(&_PyRuntime); *strong_ptr = (PyInterpreterRef)interp; - return 0; } +int +PyInterpreterWeakRef_AsStrong(PyInterpreterWeakRef wref, PyInterpreterRef *strong_ptr) +{ + assert(strong_ptr != NULL); + int64_t interp_id = wref.id; + /* Interpreters cannot be deleted while we hold the runtime lock. */ + _PyRuntimeState *runtime = &_PyRuntime; + HEAD_LOCK(runtime); + PyInterpreterState *interp = interp_look_up_id(runtime, interp_id); + if (interp == NULL) { + HEAD_UNLOCK(runtime); + *strong_ptr = 0; + return -1; + } + + int res = try_acquire_strong_ref(interp, strong_ptr); + HEAD_UNLOCK(runtime); + return res; +} + +int +PyInterpreterState_AsStrong(PyInterpreterState *interp, PyInterpreterRef *strong_ptr) +{ + assert(interp != NULL); + assert(strong_ptr != NULL); + _PyRuntimeState *runtime = &_PyRuntime; + HEAD_LOCK(runtime); + int res = try_acquire_strong_ref(interp, strong_ptr); + HEAD_UNLOCK(runtime); + + return res; +} + int PyThreadState_Ensure(PyInterpreterState *interp) { @@ -3321,5 +3344,5 @@ PyThreadState_Ensure(PyInterpreterState *interp) void PyThreadState_Release(void) { - return 0; + return; } From 16d79de65d59ef67dcd3fac00539d90f9b7d2b21 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 22 May 2025 16:42:19 -0400 Subject: [PATCH 23/61] Implement new version of PyThreadState_Ensure() and PyThreadState_Release() --- Include/cpython/pystate.h | 16 +++++------- Python/pystate.c | 52 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 85479d2c2f5017..28b488e333a90b 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -61,12 +61,6 @@ typedef struct _stack_chunk { PyObject * data[1]; /* Variable sized */ } _PyStackChunk; -typedef struct _ensured_tstate { - struct _ensured_tstate *next; - PyThreadState *prior_tstate; - uint8_t was_daemon; -} _Py_ensured_tstate; - struct _ts { /* See Python/ceval.c for comments explaining most fields */ @@ -213,10 +207,11 @@ struct _ts { PyObject *threading_local_sentinel; _PyRemoteDebuggerSupport remote_debugger_support; - /* Whether this thread hangs when the interpreter is finalizing. */ - uint8_t daemon; + /* Number of nested PyThreadState_Ensure() calls on this thread state */ + Py_ssize_t ensure_counter; - _Py_ensured_tstate *ensured; + /* Thread state that was active before PyThreadState_Ensure() was called. */ + PyThreadState *prior_ensure; }; /* other API */ @@ -279,6 +274,7 @@ PyAPI_FUNC(PyInterpreterRef) PyInterpreterRef_Get(void); PyAPI_FUNC(PyInterpreterRef) PyInterpreterRef_Dup(PyInterpreterRef ref); PyAPI_FUNC(int) PyInterpreterState_AsStrong(PyInterpreterState *interp, PyInterpreterRef *strong_ptr); PyAPI_FUNC(void) PyInterpreterRef_Close(PyInterpreterRef ref); +PyAPI_FUNC(PyInterpreterState *) PyInterpreterRef_AsInterpreter(PyInterpreterRef ref); #define PyInterpreterRef_Close(ref) do { \ PyInterpreterRef_Close(ref); \ @@ -301,6 +297,6 @@ PyAPI_FUNC(void) PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref); PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); PyAPI_FUNC(void) _PyInterpreterState_Incref(PyInterpreterState *interp); -PyAPI_FUNC(int) PyThreadState_Ensure(PyInterpreterState *interp); +PyAPI_FUNC(int) PyThreadState_Ensure(PyInterpreterRef interp_ref); PyAPI_FUNC(void) PyThreadState_Release(void); diff --git a/Python/pystate.c b/Python/pystate.c index 3599fd9053f9d8..9dc8c845318690 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3188,6 +3188,13 @@ PyInterpreterRef_Close(PyInterpreterRef ref) decref_interpreter(interp); } +PyInterpreterState * +PyInterpreterRef_AsInterpreter(PyInterpreterRef ref) +{ + PyInterpreterState *interp = ref_as_interp(ref); + return interp; +} + PyInterpreterWeakRef PyInterpreterWeakRef_Get(void) { @@ -3260,13 +3267,56 @@ PyInterpreterState_AsStrong(PyInterpreterState *interp, PyInterpreterRef *strong } int -PyThreadState_Ensure(PyInterpreterState *interp) +PyThreadState_Ensure(PyInterpreterRef interp_ref) { + PyInterpreterState *interp = ref_as_interp(interp_ref); + PyThreadState *attached_tstate = current_fast_get(); + if (attached_tstate != NULL && attached_tstate->interp == interp) { + /* Yay! We already have an attached thread state that matches. */ + ++attached_tstate->ensure_counter; + return 0; + } + + PyThreadState *detached_gilstate = gilstate_get(); + if (detached_gilstate != NULL && detached_gilstate->interp == interp) { + /* There's a detached thread state that works. */ + assert(attached_tstate == NULL); + ++detached_gilstate->ensure_counter; + _PyThreadState_Attach(detached_gilstate); + return 0; + } + + PyThreadState *fresh_tstate = _PyThreadState_NewBound(interp, + _PyThreadState_WHENCE_GILSTATE); + if (fresh_tstate == NULL) { + return -1; + } + + if (attached_tstate != NULL) { + fresh_tstate->ensure_counter = 1; + fresh_tstate->prior_ensure = PyThreadState_Swap(fresh_tstate); + } else { + _PyThreadState_Attach(fresh_tstate); + } + return 0; } void PyThreadState_Release(void) { + PyThreadState *tstate = current_fast_get(); + _Py_EnsureTstateNotNULL(tstate); + Py_ssize_t remaining = --tstate->ensure_counter; + if (remaining < 0) { + Py_FatalError("PyThreadState_Release() called more times than PyThreadState_Ensure()"); + } + if (remaining == 0) { + PyThreadState *to_restore = tstate->prior_ensure; + PyThreadState_Clear(tstate); + PyThreadState_Swap(to_restore); + PyThreadState_Delete(tstate); + } + return; } From c2bffcd4b95961a2e9ef7aa17d1e7b9ddd308a77 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 22 May 2025 17:00:52 -0400 Subject: [PATCH 24/61] Use the new APIs in the tests. --- Modules/_testcapimodule.c | 52 ++++++++++++++++++++++++--------------- Programs/_testembed.c | 18 ++++++++------ 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 06243ba225366e..b7f13dfe97fbc6 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2550,37 +2550,46 @@ static PyObject * test_interp_refcount(PyObject *self, PyObject *unused) { PyInterpreterState *interp = PyInterpreterState_Get(); + PyInterpreterRef ref1; + PyInterpreterRef ref2; // Reference counts are technically 0 by default assert(_PyInterpreterState_Refcount(interp) == 0); - PyInterpreterState *held = PyInterpreterState_Hold(); + ref1 = PyInterpreterRef_Get(); assert(_PyInterpreterState_Refcount(interp) == 1); - held = PyInterpreterState_Hold(); + ref2 = PyInterpreterRef_Get(); assert(_PyInterpreterState_Refcount(interp) == 2); - PyInterpreterState_Release(held); + PyInterpreterRef_Close(ref1); assert(_PyInterpreterState_Refcount(interp) == 1); - PyInterpreterState_Release(held); + PyInterpreterRef_Close(ref2); + assert(_PyInterpreterState_Refcount(interp) == 0); + + ref1 = PyInterpreterRef_Get(); + ref2 = PyInterpreterRef_Dup(ref1); + assert(_PyInterpreterState_Refcount(interp) == 2); + assert(PyInterpreterRef_AsInterpreter(ref1) == interp); + assert(PyInterpreterRef_AsInterpreter(ref2) == interp); + PyInterpreterRef_Close(ref1); + PyInterpreterRef_Close(ref2); assert(_PyInterpreterState_Refcount(interp) == 0); Py_RETURN_NONE; } static PyObject * -test_interp_lookup(PyObject *self, PyObject *unused) +test_interp_weak_ref(PyObject *self, PyObject *unused) { PyInterpreterState *interp = PyInterpreterState_Get(); + PyInterpreterWeakRef wref = PyInterpreterWeakRef_Get(); assert(_PyInterpreterState_Refcount(interp) == 0); - int64_t interp_id = PyInterpreterState_GetID(interp); - PyInterpreterState *ref = PyInterpreterState_Lookup(interp_id); - assert(ref == interp); + + PyInterpreterRef ref; + int res = PyInterpreterWeakRef_AsStrong(wref, &ref); + assert(res == 0); + assert(PyInterpreterRef_AsInterpreter(ref) == interp); assert(_PyInterpreterState_Refcount(interp) == 1); - PyInterpreterState_Release(ref); - assert(PyInterpreterState_Lookup(10000) == NULL); - Py_BEGIN_ALLOW_THREADS; - ref = PyInterpreterState_Lookup(interp_id); - assert(ref == interp); - PyInterpreterState_Release(ref); - Py_END_ALLOW_THREADS; + PyInterpreterWeakRef_Close(wref); + PyInterpreterRef_Close(ref); Py_RETURN_NONE; } @@ -2589,20 +2598,20 @@ static PyObject * test_interp_ensure(PyObject *self, PyObject *unused) { PyInterpreterState *interp = PyInterpreterState_Get(); + PyInterpreterRef ref = PyInterpreterRef_Get(); PyThreadState *save_tstate = PyThreadState_Swap(NULL); PyThreadState *tstate = Py_NewInterpreter(); + PyInterpreterRef sub_ref = PyInterpreterRef_Get(); PyInterpreterState *subinterp = PyThreadState_GetInterpreter(tstate); for (int i = 0; i < 10; ++i) { - _PyInterpreterState_Incref(interp); - int res = PyThreadState_Ensure(interp); + int res = PyThreadState_Ensure(sub_ref); assert(res == 0); assert(PyInterpreterState_Get() == interp); } for (int i = 0; i < 10; ++i) { - _PyInterpreterState_Incref(subinterp); - int res = PyThreadState_Ensure(subinterp); + int res = PyThreadState_Ensure(sub_ref); assert(res == 0); assert(PyInterpreterState_Get() == subinterp); } @@ -2611,6 +2620,9 @@ test_interp_ensure(PyObject *self, PyObject *unused) PyThreadState_Release(); } + PyInterpreterRef_Close(ref); + PyInterpreterRef_Close(sub_ref); + PyThreadState_Swap(save_tstate); Py_RETURN_NONE; } @@ -2710,7 +2722,7 @@ static PyMethodDef TestMethods[] = { {"code_offset_to_line", _PyCFunction_CAST(code_offset_to_line), METH_FASTCALL}, {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, {"test_interp_refcount", test_interp_refcount, METH_NOARGS}, - {"test_interp_lookup", test_interp_lookup, METH_NOARGS}, + {"test_interp_weak_ref", test_interp_weak_ref, METH_NOARGS}, {"test_interp_ensure", test_interp_ensure, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 1768f8cffd65d6..98745b65db843f 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2323,7 +2323,7 @@ const char *THREAD_CODE = "import time\n" "fib(10)"; typedef struct { - PyInterpreterState *interp; + PyInterpreterRef ref; int done; } ThreadData; @@ -2331,12 +2331,12 @@ static void do_tstate_ensure(void *arg) { ThreadData *data = (ThreadData *)arg; - int res = PyThreadState_Ensure(data->interp); + int res = PyThreadState_Ensure(data->ref); assert(res == 0); - PyThreadState_Ensure(PyInterpreterState_Hold()); - PyThreadState_Ensure(PyInterpreterState_Hold()); + PyThreadState_Ensure(data->ref); + PyThreadState_Ensure(data->ref); PyGILState_STATE gstate = PyGILState_Ensure(); - PyThreadState_Ensure(PyInterpreterState_Hold()); + PyThreadState_Ensure(data->ref); res = PyRun_SimpleString(THREAD_CODE); PyThreadState_Release(); PyGILState_Release(gstate); @@ -2344,19 +2344,21 @@ do_tstate_ensure(void *arg) PyThreadState_Release(); assert(res == 0); PyThreadState_Release(); + PyInterpreterRef_Close(data->ref); data->done = 1; } static int test_thread_state_ensure(void) { - _testembed_Py_InitializeFromConfig(); + _testembed_initialize(); PyThread_handle_t handle; PyThread_ident_t ident; - ThreadData data = { PyInterpreterState_Hold() }; + PyInterpreterRef ref = PyInterpreterRef_Get(); + ThreadData data = { ref }; if (PyThread_start_joinable_thread(do_tstate_ensure, &data, &ident, &handle) < 0) { - PyInterpreterState_Release(data.interp); + PyInterpreterRef_Close(ref); return -1; } Py_Finalize(); From 911c6b5e0ee9a399773cd245265056a623c26955 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 22 May 2025 17:03:32 -0400 Subject: [PATCH 25/61] Fix _testcapi. --- Modules/_testcapimodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index b7f13dfe97fbc6..c6c90808631f2c 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2605,7 +2605,7 @@ test_interp_ensure(PyObject *self, PyObject *unused) PyInterpreterState *subinterp = PyThreadState_GetInterpreter(tstate); for (int i = 0; i < 10; ++i) { - int res = PyThreadState_Ensure(sub_ref); + int res = PyThreadState_Ensure(ref); assert(res == 0); assert(PyInterpreterState_Get() == interp); } From b84fa9006a885105de65c3476f2d4aeafdc5baea Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 22 May 2025 17:09:55 -0400 Subject: [PATCH 26/61] Fix the ensure counter. --- Python/pystate.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/pystate.c b/Python/pystate.c index 9dc8c845318690..62085b5ccb04e5 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3291,9 +3291,9 @@ PyThreadState_Ensure(PyInterpreterRef interp_ref) if (fresh_tstate == NULL) { return -1; } + fresh_tstate->ensure_counter = 1; if (attached_tstate != NULL) { - fresh_tstate->ensure_counter = 1; fresh_tstate->prior_ensure = PyThreadState_Swap(fresh_tstate); } else { _PyThreadState_Attach(fresh_tstate); From 9ccf6bd4515ad414d03cc1319b4d319659089456 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 22 May 2025 17:12:12 -0400 Subject: [PATCH 27/61] Add the test to test_embed. --- Lib/test/test_embed.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 89f4aebe28f4a1..24d7b99ff0ff31 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -1919,6 +1919,10 @@ def test_gilstate_after_finalization(self): self.run_embedded_interpreter("test_gilstate_after_finalization") + def test_thread_state_ensure(self): + self.run_embedded_interpreter("test_thread_state_ensure") + + class MiscTests(EmbeddingTestsMixin, unittest.TestCase): def test_unicode_id_init(self): # bpo-42882: Test that _PyUnicode_FromId() works From 481caf5c2260f05ed3dcbef1974b39acb2ed07d8 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 22 May 2025 17:16:33 -0400 Subject: [PATCH 28/61] Allow the wait to be interrupted by CTRL+C. --- Python/pylifecycle.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 8d39a0e1bcaadd..9ae2c921eefbb5 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -3488,7 +3488,24 @@ wait_for_native_shutdown(PyInterpreterState *interp) } PyMutex_Unlock(&finalizing->mutex); - PyEvent_Wait(&finalizing->finished); + PyTime_t wait_ns = 1000 * 1000; // 1 millisecond + + while (true) { + if (PyEvent_WaitTimed(&finalizing->finished, wait_ns, 1)) { + // Event set + break; + } + + if (PyErr_CheckSignals()) { + // The user CTRL+C'd us, bail out without waiting for a reference + // count of zero. + // + // This will probably cause threads to crash, but maybe that's + // better than a deadlock. It might be worth intentionally + // leaking subinterpreters to prevent some crashes here. + break; + } + } } int Py_AtExit(void (*func)(void)) From 71e1aec58b583166d894ede39165b08b4127342a Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 22 May 2025 17:18:52 -0400 Subject: [PATCH 29/61] Print the error before bailing out. --- Python/pylifecycle.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 9ae2c921eefbb5..bb6cd9afd03505 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -3497,6 +3497,7 @@ wait_for_native_shutdown(PyInterpreterState *interp) } if (PyErr_CheckSignals()) { + PyErr_Print(); // The user CTRL+C'd us, bail out without waiting for a reference // count of zero. // From d66157883d854eabef0d555d2d8f5b15ae905312 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 27 May 2025 21:02:39 -0400 Subject: [PATCH 30/61] Updates for the new proposal. --- Include/cpython/pystate.h | 19 +++++++--- Modules/_testcapimodule.c | 25 +++++++++--- Programs/_testembed.c | 5 ++- Python/pystate.c | 80 ++++++++++++++++++++++++++++++--------- 4 files changed, 98 insertions(+), 31 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 28b488e333a90b..884bb026e45372 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -270,32 +270,39 @@ PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameFunc( typedef uintptr_t PyInterpreterRef; -PyAPI_FUNC(PyInterpreterRef) PyInterpreterRef_Get(void); +PyAPI_FUNC(int) PyInterpreterRef_Get(PyInterpreterRef *ptr); PyAPI_FUNC(PyInterpreterRef) PyInterpreterRef_Dup(PyInterpreterRef ref); -PyAPI_FUNC(int) PyInterpreterState_AsStrong(PyInterpreterState *interp, PyInterpreterRef *strong_ptr); +PyAPI_FUNC(int) PyInterpreterRef_Main(PyInterpreterRef *strong_ptr); PyAPI_FUNC(void) PyInterpreterRef_Close(PyInterpreterRef ref); PyAPI_FUNC(PyInterpreterState *) PyInterpreterRef_AsInterpreter(PyInterpreterRef ref); #define PyInterpreterRef_Close(ref) do { \ PyInterpreterRef_Close(ref); \ ref = 0; \ -} while (0); \ +} while (0); /* Weak interpreter references */ typedef struct _interpreter_weakref { int64_t id; Py_ssize_t refcount; -} PyInterpreterWeakRef; +} _PyInterpreterWeakRef; -PyAPI_FUNC(PyInterpreterWeakRef) PyInterpreterWeakRef_Get(void); +typedef _PyInterpreterWeakRef *PyInterpreterWeakRef; + +PyAPI_FUNC(int) PyInterpreterWeakRef_Get(PyInterpreterWeakRef *ptr); PyAPI_FUNC(PyInterpreterWeakRef) PyInterpreterWeakRef_Dup(PyInterpreterWeakRef wref); PyAPI_FUNC(int) PyInterpreterWeakRef_AsStrong(PyInterpreterWeakRef wref, PyInterpreterRef *strong_ptr); PyAPI_FUNC(void) PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref); +#define PyInterpreterWeakRef_Close(ref) do { \ + PyInterpreterWeakRef_Close(ref); \ + ref = 0; \ +} while (0); + // Exports for '_testcapi' shared extension PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); -PyAPI_FUNC(void) _PyInterpreterState_Incref(PyInterpreterState *interp); +PyAPI_FUNC(int) _PyInterpreterState_Incref(PyInterpreterState *interp); PyAPI_FUNC(int) PyThreadState_Ensure(PyInterpreterRef interp_ref); diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index c6c90808631f2c..971bdadd6b8065 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2546,6 +2546,16 @@ toggle_reftrace_printer(PyObject *ob, PyObject *arg) Py_RETURN_NONE; } +static PyInterpreterRef +get_strong_ref(void) +{ + PyInterpreterRef ref; + if (PyInterpreterRef_Get(&ref) < 0) { + Py_FatalError("strong reference should not have failed"); + } + return ref; +} + static PyObject * test_interp_refcount(PyObject *self, PyObject *unused) { @@ -2555,16 +2565,16 @@ test_interp_refcount(PyObject *self, PyObject *unused) // Reference counts are technically 0 by default assert(_PyInterpreterState_Refcount(interp) == 0); - ref1 = PyInterpreterRef_Get(); + ref1 = get_strong_ref(); assert(_PyInterpreterState_Refcount(interp) == 1); - ref2 = PyInterpreterRef_Get(); + ref2 = get_strong_ref(); assert(_PyInterpreterState_Refcount(interp) == 2); PyInterpreterRef_Close(ref1); assert(_PyInterpreterState_Refcount(interp) == 1); PyInterpreterRef_Close(ref2); assert(_PyInterpreterState_Refcount(interp) == 0); - ref1 = PyInterpreterRef_Get(); + ref1 = get_strong_ref(); ref2 = PyInterpreterRef_Dup(ref1); assert(_PyInterpreterState_Refcount(interp) == 2); assert(PyInterpreterRef_AsInterpreter(ref1) == interp); @@ -2580,7 +2590,10 @@ static PyObject * test_interp_weak_ref(PyObject *self, PyObject *unused) { PyInterpreterState *interp = PyInterpreterState_Get(); - PyInterpreterWeakRef wref = PyInterpreterWeakRef_Get(); + PyInterpreterWeakRef wref; + if (PyInterpreterWeakRef_Get(&wref) < 0) { + return NULL; + } assert(_PyInterpreterState_Refcount(interp) == 0); PyInterpreterRef ref; @@ -2598,10 +2611,10 @@ static PyObject * test_interp_ensure(PyObject *self, PyObject *unused) { PyInterpreterState *interp = PyInterpreterState_Get(); - PyInterpreterRef ref = PyInterpreterRef_Get(); + PyInterpreterRef ref = get_strong_ref(); PyThreadState *save_tstate = PyThreadState_Swap(NULL); PyThreadState *tstate = Py_NewInterpreter(); - PyInterpreterRef sub_ref = PyInterpreterRef_Get(); + PyInterpreterRef sub_ref = get_strong_ref(); PyInterpreterState *subinterp = PyThreadState_GetInterpreter(tstate); for (int i = 0; i < 10; ++i) { diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 98745b65db843f..6e60765e32afeb 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2354,7 +2354,10 @@ test_thread_state_ensure(void) _testembed_initialize(); PyThread_handle_t handle; PyThread_ident_t ident; - PyInterpreterRef ref = PyInterpreterRef_Get(); + PyInterpreterRef ref; + if (PyInterpreterRef_Get(&ref) < 0) { + return -1; + }; ThreadData data = { ref }; if (PyThread_start_joinable_thread(do_tstate_ensure, &data, &ident, &handle) < 0) { diff --git a/Python/pystate.c b/Python/pystate.c index 62085b5ccb04e5..00cf426e727430 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3145,12 +3145,22 @@ _PyInterpreterState_Refcount(PyInterpreterState *interp) return refcount; } -void +int _PyInterpreterState_Incref(PyInterpreterState *interp) { assert(interp != NULL); - assert(_Py_atomic_load_ssize_relaxed(&interp->threads.finalizing.countdown) >= 0); + struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; + assert(_Py_atomic_load_ssize_relaxed(&finalizing->countdown) >= 0); + PyMutex *mutex = &finalizing->mutex; + PyMutex_Lock(mutex); + if (_PyEvent_IsSet(&finalizing->finished)) { + PyMutex_Unlock(mutex); + return -1; + } + _Py_atomic_add_ssize(&interp->threads.finalizing.countdown, 1); + PyMutex_Unlock(mutex); + return 0; } static PyInterpreterState * @@ -3164,19 +3174,29 @@ ref_as_interp(PyInterpreterRef ref) return interp; } -PyInterpreterRef -PyInterpreterRef_Get(void) +int +PyInterpreterRef_Get(PyInterpreterRef *ref_ptr) { + assert(ref_ptr != NULL); PyInterpreterState *interp = PyInterpreterState_Get(); - _PyInterpreterState_Incref(interp); - return (PyInterpreterRef)interp; + if (_PyInterpreterState_Incref(interp) < 0) { + PyErr_SetString(PyExc_PythonFinalizationError, + "Cannot acquire strong interpreter references anymore"); + return -1; + } + *ref_ptr = (PyInterpreterRef)interp; + return 0; } PyInterpreterRef PyInterpreterRef_Dup(PyInterpreterRef ref) { PyInterpreterState *interp = ref_as_interp(ref); - _PyInterpreterState_Incref(interp); + int res = _PyInterpreterState_Incref(interp); + (void)res; + // We already hold a strong reference, so it shouldn't be possible + // for the interpreter to be at a point where references don't work anymore + assert(res == 0); return (PyInterpreterRef)interp; } @@ -3195,24 +3215,48 @@ PyInterpreterRef_AsInterpreter(PyInterpreterRef ref) return interp; } -PyInterpreterWeakRef -PyInterpreterWeakRef_Get(void) +int +PyInterpreterWeakRef_Get(PyInterpreterWeakRef *wref_ptr) { PyInterpreterState *interp = PyInterpreterState_Get(); - PyInterpreterWeakRef wref = { interp->id }; + _PyInterpreterWeakRef *wref = PyMem_RawMalloc(sizeof(_PyInterpreterWeakRef)); + if (wref == NULL) { + PyErr_NoMemory(); + return -1; + } + wref->refcount = 1; + wref->id = interp->id; + *wref_ptr = (PyInterpreterWeakRef)wref; + return 0; +} + +static _PyInterpreterWeakRef * +wref_handle_as_ptr(PyInterpreterWeakRef wref_handle) +{ + _PyInterpreterWeakRef *wref = (_PyInterpreterWeakRef *)wref_handle; + if (wref == NULL) { + Py_FatalError("Got a null weak interpreter reference, likely due to use after close."); + } + return wref; } PyInterpreterWeakRef -PyInterpreterWeakRef_Dup(PyInterpreterWeakRef wref) +PyInterpreterWeakRef_Dup(PyInterpreterWeakRef wref_handle) { + _PyInterpreterWeakRef *wref = wref_handle_as_ptr(wref_handle); + ++wref->refcount; return wref; } +#undef PyInterpreterWeakRef_Close void -PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref) +PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref_handle) { - return; + _PyInterpreterWeakRef *wref = wref_handle_as_ptr(wref_handle); + if (--wref->refcount == 0) { + PyMem_RawFree(wref); + } } static int @@ -3234,10 +3278,11 @@ try_acquire_strong_ref(PyInterpreterState *interp, PyInterpreterRef *strong_ptr) } int -PyInterpreterWeakRef_AsStrong(PyInterpreterWeakRef wref, PyInterpreterRef *strong_ptr) +PyInterpreterWeakRef_AsStrong(PyInterpreterWeakRef wref_handle, PyInterpreterRef *strong_ptr) { assert(strong_ptr != NULL); - int64_t interp_id = wref.id; + _PyInterpreterWeakRef *wref = wref_handle_as_ptr(wref_handle); + int64_t interp_id = wref->id; /* Interpreters cannot be deleted while we hold the runtime lock. */ _PyRuntimeState *runtime = &_PyRuntime; HEAD_LOCK(runtime); @@ -3254,13 +3299,12 @@ PyInterpreterWeakRef_AsStrong(PyInterpreterWeakRef wref, PyInterpreterRef *stron } int -PyInterpreterState_AsStrong(PyInterpreterState *interp, PyInterpreterRef *strong_ptr) +PyInterpreterRef_Main(PyInterpreterRef *strong_ptr) { - assert(interp != NULL); assert(strong_ptr != NULL); _PyRuntimeState *runtime = &_PyRuntime; HEAD_LOCK(runtime); - int res = try_acquire_strong_ref(interp, strong_ptr); + int res = try_acquire_strong_ref(&runtime->_main_interpreter, strong_ptr); HEAD_UNLOCK(runtime); return res; From 4249c5d6cd0188bb719b506dcf62fe6d848a5e08 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 27 May 2025 21:19:28 -0400 Subject: [PATCH 31/61] Bikeshedding. --- Python/pylifecycle.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index bb6cd9afd03505..8cd630fc9aab8a 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -98,7 +98,7 @@ static PyStatus init_android_streams(PyThreadState *tstate); static PyStatus init_apple_streams(PyThreadState *tstate); #endif static void wait_for_thread_shutdown(PyThreadState *tstate); -static void wait_for_native_shutdown(PyInterpreterState *interp); +static void wait_for_interp_references(PyInterpreterState *interp); static void finalize_subinterpreters(void); static void call_ll_exitfuncs(_PyRuntimeState *runtime); @@ -2025,7 +2025,7 @@ _Py_Finalize(_PyRuntimeState *runtime) wait_for_thread_shutdown(tstate); // Wait for the interpreter's reference count to reach zero - wait_for_native_shutdown(tstate->interp); + wait_for_interp_references(tstate->interp); // Make any remaining pending calls. _Py_FinishPendingCalls(tstate); @@ -2444,7 +2444,7 @@ Py_EndInterpreter(PyThreadState *tstate) wait_for_thread_shutdown(tstate); // Wait for the interpreter's reference count to reach zero - wait_for_native_shutdown(tstate->interp); + wait_for_interp_references(tstate->interp); // Make any remaining pending calls. _Py_FinishPendingCalls(tstate); @@ -3472,10 +3472,10 @@ wait_for_thread_shutdown(PyThreadState *tstate) Py_DECREF(threading); } -/* Wait for all non-daemon native threads to finish. +/* Wait for the interpreter's reference count to reach zero. See PEP 788. */ static void -wait_for_native_shutdown(PyInterpreterState *interp) +wait_for_interp_references(PyInterpreterState *interp) { assert(interp != NULL); struct _Py_finalizing_threads *finalizing = &interp->threads.finalizing; From 71f2fd7d46831209a31dc18b22f493ba58852e9d Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 28 May 2025 12:43:18 -0400 Subject: [PATCH 32/61] Apply suggestions from code review Co-authored-by: Victor Stinner --- Include/cpython/pystate.h | 6 +++--- Programs/_testembed.c | 4 +++- Python/pystate.c | 10 +++++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 884bb026e45372..4b03691d80587c 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -279,11 +279,11 @@ PyAPI_FUNC(PyInterpreterState *) PyInterpreterRef_AsInterpreter(PyInterpreterRef #define PyInterpreterRef_Close(ref) do { \ PyInterpreterRef_Close(ref); \ ref = 0; \ -} while (0); +} while (0) /* Weak interpreter references */ -typedef struct _interpreter_weakref { +typedef struct _PyInterpreterWeakRef { int64_t id; Py_ssize_t refcount; } _PyInterpreterWeakRef; @@ -298,7 +298,7 @@ PyAPI_FUNC(void) PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref); #define PyInterpreterWeakRef_Close(ref) do { \ PyInterpreterWeakRef_Close(ref); \ ref = 0; \ -} while (0); +} while (0) // Exports for '_testcapi' shared extension PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 6e60765e32afeb..c5bb0246ce3c2b 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2313,7 +2313,9 @@ test_get_incomplete_frame(void) return result; } -const char *THREAD_CODE = "import time\n" +const char *THREAD_CODE = \ + "import time\n" + ... "time.sleep(0.2)\n" "def fib(n):\n" " if n <= 1:\n" diff --git a/Python/pystate.c b/Python/pystate.c index 00cf426e727430..af867e88858b05 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3141,8 +3141,7 @@ Py_ssize_t _PyInterpreterState_Refcount(PyInterpreterState *interp) { assert(interp != NULL); - Py_ssize_t refcount = _Py_atomic_load_ssize_relaxed(&interp->threads.finalizing.countdown); - return refcount; + return _Py_atomic_load_ssize_relaxed(&interp->threads.finalizing.countdown); } int @@ -3168,7 +3167,7 @@ ref_as_interp(PyInterpreterRef ref) { PyInterpreterState *interp = (PyInterpreterState *)ref; if (interp == NULL) { - Py_FatalError("Got a null interpreter reference, likely due to use after close."); + Py_FatalError("Got a null interpreter reference, likely due to use after PyInterpreterRef_Close()"); } return interp; @@ -3235,7 +3234,7 @@ wref_handle_as_ptr(PyInterpreterWeakRef wref_handle) { _PyInterpreterWeakRef *wref = (_PyInterpreterWeakRef *)wref_handle; if (wref == NULL) { - Py_FatalError("Got a null weak interpreter reference, likely due to use after close."); + Py_FatalError("Got a null weak interpreter reference, likely due to use after PyInterpreterWeakRef_Close()"); } return wref; @@ -3269,7 +3268,8 @@ try_acquire_strong_ref(PyInterpreterState *interp, PyInterpreterRef *strong_ptr) /* Interpreter has already finished threads */ *strong_ptr = 0; return -1; - } else { + } + else { _Py_atomic_add_ssize(&finalizing->countdown, 1); } PyMutex_Unlock(mutex); From 03ccb38c85c1f24df3411d5baee4ebe9a7b7ce5a Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 28 May 2025 20:24:42 -0400 Subject: [PATCH 33/61] Fix failing build. --- Programs/_testembed.c | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index c5bb0246ce3c2b..8fff40910d2fa1 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2314,15 +2314,14 @@ test_get_incomplete_frame(void) } const char *THREAD_CODE = \ - "import time\n" - ... - "time.sleep(0.2)\n" - "def fib(n):\n" - " if n <= 1:\n" - " return n\n" - " else:\n" - " return fib(n - 1) + fib(n - 2)\n" - "fib(10)"; + "import time\n" + "time.sleep(0.2)\n" + "def fib(n):\n" + " if n <= 1:\n" + " return n\n" + " else:\n" + " return fib(n - 1) + fib(n - 2)\n" + "fib(10)"; typedef struct { PyInterpreterRef ref; From bca65fb2f810aa4e7f981c5a4078078d702af688 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 28 May 2025 20:36:21 -0400 Subject: [PATCH 34/61] Rename parameter. --- Python/pystate.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Python/pystate.c b/Python/pystate.c index af867e88858b05..a55f22f020256b 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3174,16 +3174,16 @@ ref_as_interp(PyInterpreterRef ref) } int -PyInterpreterRef_Get(PyInterpreterRef *ref_ptr) +PyInterpreterRef_Get(PyInterpreterRef *ref) { - assert(ref_ptr != NULL); + assert(ref != NULL); PyInterpreterState *interp = PyInterpreterState_Get(); if (_PyInterpreterState_Incref(interp) < 0) { PyErr_SetString(PyExc_PythonFinalizationError, "Cannot acquire strong interpreter references anymore"); return -1; } - *ref_ptr = (PyInterpreterRef)interp; + *ref = (PyInterpreterRef)interp; return 0; } From 510ade14df1dc082640448ab1ff22529360d93a8 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 28 May 2025 20:37:06 -0400 Subject: [PATCH 35/61] Fix formatting. --- Lib/test/test_embed.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 24d7b99ff0ff31..7e663c035b9a2e 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -1914,11 +1914,9 @@ def test_audit_run_stdin(self): def test_get_incomplete_frame(self): self.run_embedded_interpreter("test_get_incomplete_frame") - def test_gilstate_after_finalization(self): self.run_embedded_interpreter("test_gilstate_after_finalization") - def test_thread_state_ensure(self): self.run_embedded_interpreter("test_thread_state_ensure") From 39714088c16a0c7af9b64a05e52123f9b210c0a3 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 28 May 2025 20:38:16 -0400 Subject: [PATCH 36/61] Add tstate check. --- Modules/_testcapimodule.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 971bdadd6b8065..96ab2ef53984ed 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2617,9 +2617,11 @@ test_interp_ensure(PyObject *self, PyObject *unused) PyInterpreterRef sub_ref = get_strong_ref(); PyInterpreterState *subinterp = PyThreadState_GetInterpreter(tstate); + assert(PyThreadState_GetUnchecked() == NULL); for (int i = 0; i < 10; ++i) { int res = PyThreadState_Ensure(ref); assert(res == 0); + assert(PyThreadState_GetUnchecked() != NULL); assert(PyInterpreterState_Get() == interp); } From 6c4c52bff7371f2a160d3fcc730114269f7e6894 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 28 May 2025 20:39:36 -0400 Subject: [PATCH 37/61] Move to pycore_pystate.h --- Include/cpython/pystate.h | 4 ---- Include/internal/pycore_pystate.h | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 4b03691d80587c..56ec268586c5e4 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -300,10 +300,6 @@ PyAPI_FUNC(void) PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref); ref = 0; \ } while (0) -// Exports for '_testcapi' shared extension -PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); -PyAPI_FUNC(int) _PyInterpreterState_Incref(PyInterpreterState *interp); - PyAPI_FUNC(int) PyThreadState_Ensure(PyInterpreterRef interp_ref); PyAPI_FUNC(void) PyThreadState_Release(void); diff --git a/Include/internal/pycore_pystate.h b/Include/internal/pycore_pystate.h index 633e5cf77db918..c8610e200d05f5 100644 --- a/Include/internal/pycore_pystate.h +++ b/Include/internal/pycore_pystate.h @@ -328,6 +328,10 @@ _Py_RecursionLimit_GetMargin(PyThreadState *tstate) return Py_ARITHMETIC_RIGHT_SHIFT(intptr_t, here_addr - (intptr_t)_tstate->c_stack_soft_limit, PYOS_STACK_MARGIN_SHIFT); } +// Exports for '_testcapi' shared extension +PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); +PyAPI_FUNC(int) _PyInterpreterState_Incref(PyInterpreterState *interp); + #ifdef __cplusplus } #endif From 64920a80338e4fef5dfc14801f02ad50d7299bb1 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 28 May 2025 20:41:10 -0400 Subject: [PATCH 38/61] Revert "Move to pycore_pystate.h" This reverts commit 6c4c52bff7371f2a160d3fcc730114269f7e6894. --- Include/cpython/pystate.h | 4 ++++ Include/internal/pycore_pystate.h | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 56ec268586c5e4..4b03691d80587c 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -300,6 +300,10 @@ PyAPI_FUNC(void) PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref); ref = 0; \ } while (0) +// Exports for '_testcapi' shared extension +PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); +PyAPI_FUNC(int) _PyInterpreterState_Incref(PyInterpreterState *interp); + PyAPI_FUNC(int) PyThreadState_Ensure(PyInterpreterRef interp_ref); PyAPI_FUNC(void) PyThreadState_Release(void); diff --git a/Include/internal/pycore_pystate.h b/Include/internal/pycore_pystate.h index c8610e200d05f5..633e5cf77db918 100644 --- a/Include/internal/pycore_pystate.h +++ b/Include/internal/pycore_pystate.h @@ -328,10 +328,6 @@ _Py_RecursionLimit_GetMargin(PyThreadState *tstate) return Py_ARITHMETIC_RIGHT_SHIFT(intptr_t, here_addr - (intptr_t)_tstate->c_stack_soft_limit, PYOS_STACK_MARGIN_SHIFT); } -// Exports for '_testcapi' shared extension -PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); -PyAPI_FUNC(int) _PyInterpreterState_Incref(PyInterpreterState *interp); - #ifdef __cplusplus } #endif From 05436f3a2ccdf0fba03200a8b30ae613147e1e1a Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 28 May 2025 20:46:17 -0400 Subject: [PATCH 39/61] Use an exponential wait time for the event. --- Python/pylifecycle.c | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 8cd630fc9aab8a..f2714611611c74 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -3488,7 +3488,8 @@ wait_for_interp_references(PyInterpreterState *interp) } PyMutex_Unlock(&finalizing->mutex); - PyTime_t wait_ns = 1000 * 1000; // 1 millisecond + PyTime_t wait_max = 1000 * 1000 * 100; // 100 milliseconds + PyTime_t wait_ns = 1000; // 1 microsecond while (true) { if (PyEvent_WaitTimed(&finalizing->finished, wait_ns, 1)) { @@ -3496,14 +3497,17 @@ wait_for_interp_references(PyInterpreterState *interp) break; } + wait_ns *= 2; + wait_ns = Py_MIN(wait_ns, wait_max); + if (PyErr_CheckSignals()) { PyErr_Print(); - // The user CTRL+C'd us, bail out without waiting for a reference - // count of zero. - // - // This will probably cause threads to crash, but maybe that's - // better than a deadlock. It might be worth intentionally - // leaking subinterpreters to prevent some crashes here. + /* The user CTRL+C'd us, bail out without waiting for a reference + count of zero. + + This will probably cause threads to crash, but maybe that's + better than a deadlock. It might be worth intentionally + leaking subinterpreters to prevent some crashes here. */ break; } } From 02bc2d79b468114c49c17cef36f45da4e1a92cde Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 28 May 2025 20:47:56 -0400 Subject: [PATCH 40/61] Mark function as static. --- Python/pystate.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/pystate.c b/Python/pystate.c index a55f22f020256b..771611d968d1a0 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1294,7 +1294,7 @@ interp_look_up_id(_PyRuntimeState *runtime, int64_t requested_id) return NULL; } -PyInterpreterState * +static PyInterpreterState * _PyInterpreterState_LookUpIDNoErr(int64_t requested_id) { PyInterpreterState *interp = NULL; From 03fa2af3596020941ee55f690bfe8b43246548ca Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 28 May 2025 20:49:41 -0400 Subject: [PATCH 41/61] Update fatal error message. --- Python/pystate.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Python/pystate.c b/Python/pystate.c index 771611d968d1a0..60a40e91860367 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1749,7 +1749,8 @@ decref_interpreter(PyInterpreterState *interp) if (old == 1 && shutting_down_natives(interp)) { _PyEvent_Notify(&finalizing->finished); } else if (old <= 0) { - Py_FatalError("interpreter has negative reference count"); + Py_FatalError("interpreter has negative reference count, likely due" + " to an extra PyInterpreterRef_Close()"); } } From b955d8564d00ecc27adbf01a8ee51b9094e447b0 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 28 May 2025 20:51:33 -0400 Subject: [PATCH 42/61] Remove incorrect assertion. --- Modules/_testcapimodule.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 96ab2ef53984ed..e9451b684a14c7 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2617,7 +2617,6 @@ test_interp_ensure(PyObject *self, PyObject *unused) PyInterpreterRef sub_ref = get_strong_ref(); PyInterpreterState *subinterp = PyThreadState_GetInterpreter(tstate); - assert(PyThreadState_GetUnchecked() == NULL); for (int i = 0; i < 10; ++i) { int res = PyThreadState_Ensure(ref); assert(res == 0); From 523670075c05e339fe5bc420f96ee2b6f3bbf718 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 29 May 2025 06:39:36 -0400 Subject: [PATCH 43/61] Add a comment regarding PyMem_RawMalloc() --- Python/pystate.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Python/pystate.c b/Python/pystate.c index 60a40e91860367..7c2078ecc61818 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3219,6 +3219,8 @@ int PyInterpreterWeakRef_Get(PyInterpreterWeakRef *wref_ptr) { PyInterpreterState *interp = PyInterpreterState_Get(); + /* PyInterpreterWeakRef_Close() can be called without an attached thread + state, so we have to use the raw allocator. */ _PyInterpreterWeakRef *wref = PyMem_RawMalloc(sizeof(_PyInterpreterWeakRef)); if (wref == NULL) { PyErr_NoMemory(); From a2771309fb3727c3a5581c2950d6a58adf2f8bc6 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 10:33:34 -0400 Subject: [PATCH 44/61] Add a comment. --- Programs/_testembed.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 8fff40910d2fa1..7da334b54242dd 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2365,6 +2365,9 @@ test_thread_state_ensure(void) PyInterpreterRef_Close(ref); return -1; } + // We hold a strong interpreter reference, so we don't + // have to worry about the interpreter shutting down before + // we finalize. Py_Finalize(); assert(data.done == 1); return 0; From dac0c1a1b61682d547d9f0e1abc05606b8f81d87 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 10:34:38 -0400 Subject: [PATCH 45/61] Update Include/cpython/pystate.h Co-authored-by: Victor Stinner --- Include/cpython/pystate.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 4b03691d80587c..82f3886363a630 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -270,9 +270,9 @@ PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameFunc( typedef uintptr_t PyInterpreterRef; -PyAPI_FUNC(int) PyInterpreterRef_Get(PyInterpreterRef *ptr); +PyAPI_FUNC(int) PyInterpreterRef_Get(PyInterpreterRef *ref); PyAPI_FUNC(PyInterpreterRef) PyInterpreterRef_Dup(PyInterpreterRef ref); -PyAPI_FUNC(int) PyInterpreterRef_Main(PyInterpreterRef *strong_ptr); +PyAPI_FUNC(int) PyInterpreterRef_Main(PyInterpreterRef *ref); PyAPI_FUNC(void) PyInterpreterRef_Close(PyInterpreterRef ref); PyAPI_FUNC(PyInterpreterState *) PyInterpreterRef_AsInterpreter(PyInterpreterRef ref); From 79a1852e4e7e7ea245ce3918461eeda5a2fd6095 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 10:40:45 -0400 Subject: [PATCH 46/61] Move some tests around to prevent exposure of the private API. --- Include/cpython/pystate.h | 3 --- Include/internal/pycore_pystate.h | 4 +++ Modules/_testcapimodule.c | 32 ------------------------ Modules/_testinternalcapi.c | 41 +++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 82f3886363a630..76793816af7fa9 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -300,9 +300,6 @@ PyAPI_FUNC(void) PyInterpreterWeakRef_Close(PyInterpreterWeakRef wref); ref = 0; \ } while (0) -// Exports for '_testcapi' shared extension -PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); -PyAPI_FUNC(int) _PyInterpreterState_Incref(PyInterpreterState *interp); PyAPI_FUNC(int) PyThreadState_Ensure(PyInterpreterRef interp_ref); diff --git a/Include/internal/pycore_pystate.h b/Include/internal/pycore_pystate.h index 633e5cf77db918..9f6ec120317592 100644 --- a/Include/internal/pycore_pystate.h +++ b/Include/internal/pycore_pystate.h @@ -328,6 +328,10 @@ _Py_RecursionLimit_GetMargin(PyThreadState *tstate) return Py_ARITHMETIC_RIGHT_SHIFT(intptr_t, here_addr - (intptr_t)_tstate->c_stack_soft_limit, PYOS_STACK_MARGIN_SHIFT); } +// Exports for '_testinternalcapi' shared extension +PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_Refcount(PyInterpreterState *interp); +PyAPI_FUNC(int) _PyInterpreterState_Incref(PyInterpreterState *interp); + #ifdef __cplusplus } #endif diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index e9451b684a14c7..cc2888a1bcffd3 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2556,36 +2556,6 @@ get_strong_ref(void) return ref; } -static PyObject * -test_interp_refcount(PyObject *self, PyObject *unused) -{ - PyInterpreterState *interp = PyInterpreterState_Get(); - PyInterpreterRef ref1; - PyInterpreterRef ref2; - - // Reference counts are technically 0 by default - assert(_PyInterpreterState_Refcount(interp) == 0); - ref1 = get_strong_ref(); - assert(_PyInterpreterState_Refcount(interp) == 1); - ref2 = get_strong_ref(); - assert(_PyInterpreterState_Refcount(interp) == 2); - PyInterpreterRef_Close(ref1); - assert(_PyInterpreterState_Refcount(interp) == 1); - PyInterpreterRef_Close(ref2); - assert(_PyInterpreterState_Refcount(interp) == 0); - - ref1 = get_strong_ref(); - ref2 = PyInterpreterRef_Dup(ref1); - assert(_PyInterpreterState_Refcount(interp) == 2); - assert(PyInterpreterRef_AsInterpreter(ref1) == interp); - assert(PyInterpreterRef_AsInterpreter(ref2) == interp); - PyInterpreterRef_Close(ref1); - PyInterpreterRef_Close(ref2); - assert(_PyInterpreterState_Refcount(interp) == 0); - - Py_RETURN_NONE; -} - static PyObject * test_interp_weak_ref(PyObject *self, PyObject *unused) { @@ -2600,7 +2570,6 @@ test_interp_weak_ref(PyObject *self, PyObject *unused) int res = PyInterpreterWeakRef_AsStrong(wref, &ref); assert(res == 0); assert(PyInterpreterRef_AsInterpreter(ref) == interp); - assert(_PyInterpreterState_Refcount(interp) == 1); PyInterpreterWeakRef_Close(wref); PyInterpreterRef_Close(ref); @@ -2735,7 +2704,6 @@ static PyMethodDef TestMethods[] = { {"test_atexit", test_atexit, METH_NOARGS}, {"code_offset_to_line", _PyCFunction_CAST(code_offset_to_line), METH_FASTCALL}, {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, - {"test_interp_refcount", test_interp_refcount, METH_NOARGS}, {"test_interp_weak_ref", test_interp_weak_ref, METH_NOARGS}, {"test_interp_ensure", test_interp_ensure, METH_NOARGS}, {NULL, NULL} /* sentinel */ diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 136e6a7a015049..a8d0f4e1e0b38e 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2342,6 +2342,46 @@ incref_decref_delayed(PyObject *self, PyObject *op) Py_RETURN_NONE; } +static PyInterpreterRef +get_strong_ref(void) +{ + PyInterpreterRef ref; + if (PyInterpreterRef_Get(&ref) < 0) { + Py_FatalError("strong reference should not have failed"); + } + return ref; +} + +static PyObject * +test_interp_refcount(PyObject *self, PyObject *unused) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + PyInterpreterRef ref1; + PyInterpreterRef ref2; + + // Reference counts are technically 0 by default + assert(_PyInterpreterState_Refcount(interp) == 0); + ref1 = get_strong_ref(); + assert(_PyInterpreterState_Refcount(interp) == 1); + ref2 = get_strong_ref(); + assert(_PyInterpreterState_Refcount(interp) == 2); + PyInterpreterRef_Close(ref1); + assert(_PyInterpreterState_Refcount(interp) == 1); + PyInterpreterRef_Close(ref2); + assert(_PyInterpreterState_Refcount(interp) == 0); + + ref1 = get_strong_ref(); + ref2 = PyInterpreterRef_Dup(ref1); + assert(_PyInterpreterState_Refcount(interp) == 2); + assert(PyInterpreterRef_AsInterpreter(ref1) == interp); + assert(PyInterpreterRef_AsInterpreter(ref2) == interp); + PyInterpreterRef_Close(ref1); + PyInterpreterRef_Close(ref2); + assert(_PyInterpreterState_Refcount(interp) == 0); + + Py_RETURN_NONE; +} + static PyMethodDef module_functions[] = { {"get_configs", get_configs, METH_NOARGS}, {"get_recursion_depth", get_recursion_depth, METH_NOARGS}, @@ -2444,6 +2484,7 @@ static PyMethodDef module_functions[] = { {"is_static_immortal", is_static_immortal, METH_O}, {"incref_decref_delayed", incref_decref_delayed, METH_O}, GET_NEXT_DICT_KEYS_VERSION_METHODDEF + {"test_interp_refcount", test_interp_refcount, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; From 47957b8c890b463214c5600d6305a3c54040d7ce Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 10:42:48 -0400 Subject: [PATCH 47/61] Move weakref test to internal C API. --- Modules/_testcapimodule.c | 21 --------------------- Modules/_testinternalcapi.c | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index cc2888a1bcffd3..66368828112611 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2556,26 +2556,6 @@ get_strong_ref(void) return ref; } -static PyObject * -test_interp_weak_ref(PyObject *self, PyObject *unused) -{ - PyInterpreterState *interp = PyInterpreterState_Get(); - PyInterpreterWeakRef wref; - if (PyInterpreterWeakRef_Get(&wref) < 0) { - return NULL; - } - assert(_PyInterpreterState_Refcount(interp) == 0); - - PyInterpreterRef ref; - int res = PyInterpreterWeakRef_AsStrong(wref, &ref); - assert(res == 0); - assert(PyInterpreterRef_AsInterpreter(ref) == interp); - PyInterpreterWeakRef_Close(wref); - PyInterpreterRef_Close(ref); - - Py_RETURN_NONE; -} - static PyObject * test_interp_ensure(PyObject *self, PyObject *unused) { @@ -2704,7 +2684,6 @@ static PyMethodDef TestMethods[] = { {"test_atexit", test_atexit, METH_NOARGS}, {"code_offset_to_line", _PyCFunction_CAST(code_offset_to_line), METH_FASTCALL}, {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, - {"test_interp_weak_ref", test_interp_weak_ref, METH_NOARGS}, {"test_interp_ensure", test_interp_ensure, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index a8d0f4e1e0b38e..6e2a311b6ce57d 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2382,6 +2382,27 @@ test_interp_refcount(PyObject *self, PyObject *unused) Py_RETURN_NONE; } +static PyObject * +test_interp_weakref_incref(PyObject *self, PyObject *unused) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + PyInterpreterWeakRef wref; + if (PyInterpreterWeakRef_Get(&wref) < 0) { + return NULL; + } + assert(_PyInterpreterState_Refcount(interp) == 0); + + PyInterpreterRef ref; + int res = PyInterpreterWeakRef_AsStrong(wref, &ref); + assert(res == 0); + assert(PyInterpreterRef_AsInterpreter(ref) == interp); + assert(_PyInterpreterState_Refcount(interp) == 1); + PyInterpreterWeakRef_Close(wref); + PyInterpreterRef_Close(ref); + + Py_RETURN_NONE; +} + static PyMethodDef module_functions[] = { {"get_configs", get_configs, METH_NOARGS}, {"get_recursion_depth", get_recursion_depth, METH_NOARGS}, @@ -2485,6 +2506,7 @@ static PyMethodDef module_functions[] = { {"incref_decref_delayed", incref_decref_delayed, METH_O}, GET_NEXT_DICT_KEYS_VERSION_METHODDEF {"test_interp_refcount", test_interp_refcount, METH_NOARGS}, + {"test_interp_weakref_incref", test_interp_weakref_incref, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; From 771d7edf0a92afcb6200998f31d7738e6fa9d32d Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 10:48:21 -0400 Subject: [PATCH 48/61] Improve reference counting tests. --- Modules/_testinternalcapi.c | 54 ++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 6e2a311b6ce57d..4a545bec8ff4e1 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2352,32 +2352,23 @@ get_strong_ref(void) return ref; } +#define NUM_REFS 100 + static PyObject * test_interp_refcount(PyObject *self, PyObject *unused) { PyInterpreterState *interp = PyInterpreterState_Get(); - PyInterpreterRef ref1; - PyInterpreterRef ref2; - - // Reference counts are technically 0 by default - assert(_PyInterpreterState_Refcount(interp) == 0); - ref1 = get_strong_ref(); - assert(_PyInterpreterState_Refcount(interp) == 1); - ref2 = get_strong_ref(); - assert(_PyInterpreterState_Refcount(interp) == 2); - PyInterpreterRef_Close(ref1); - assert(_PyInterpreterState_Refcount(interp) == 1); - PyInterpreterRef_Close(ref2); assert(_PyInterpreterState_Refcount(interp) == 0); + PyInterpreterRef refs[NUM_REFS]; + for (int i = 0; i < NUM_REFS; ++i) { + int res = PyInterpreterRef_Get(&refs[i]); + assert(_PyInterpreterState_Refcount(interp) == i + 1); + } - ref1 = get_strong_ref(); - ref2 = PyInterpreterRef_Dup(ref1); - assert(_PyInterpreterState_Refcount(interp) == 2); - assert(PyInterpreterRef_AsInterpreter(ref1) == interp); - assert(PyInterpreterRef_AsInterpreter(ref2) == interp); - PyInterpreterRef_Close(ref1); - PyInterpreterRef_Close(ref2); - assert(_PyInterpreterState_Refcount(interp) == 0); + for (int i = 0; i < NUM_REFS; ++i) { + PyInterpreterRef_Close(refs[i]); + assert(_PyInterpreterState_Refcount(interp) == (NUM_REFS - i)); + } Py_RETURN_NONE; } @@ -2392,17 +2383,26 @@ test_interp_weakref_incref(PyObject *self, PyObject *unused) } assert(_PyInterpreterState_Refcount(interp) == 0); - PyInterpreterRef ref; - int res = PyInterpreterWeakRef_AsStrong(wref, &ref); - assert(res == 0); - assert(PyInterpreterRef_AsInterpreter(ref) == interp); - assert(_PyInterpreterState_Refcount(interp) == 1); - PyInterpreterWeakRef_Close(wref); - PyInterpreterRef_Close(ref); + PyInterpreterRef refs[NUM_REFS]; + + for (int i = 0; i < NUM_REFS; ++i) { + int res = PyInterpreterWeakRef_AsStrong(wref, &refs[i]); + assert(res == 0); + assert(PyInterpreterRef_AsInterpreter(refs[i]) == interp); + assert(_PyInterpreterState_Refcount(interp) == i + 1); + } + for (int i = 0; i < NUM_REFS; ++i) { + PyInterpreterRef_Close(refs[i]); + assert(_PyInterpreterState_Refcount(interp) == (NUM_REFS - i)); + } + + PyInterpreterWeakRef_Close(wref); Py_RETURN_NONE; } +#undef NUM_REFS + static PyMethodDef module_functions[] = { {"get_configs", get_configs, METH_NOARGS}, {"get_recursion_depth", get_recursion_depth, METH_NOARGS}, From 0c3c1c7f06791149e44568758cda4dd9458f34ed Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 10:48:39 -0400 Subject: [PATCH 49/61] Remove dead function. --- Modules/_testinternalcapi.c | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 4a545bec8ff4e1..35f9a56d226aea 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2342,16 +2342,6 @@ incref_decref_delayed(PyObject *self, PyObject *op) Py_RETURN_NONE; } -static PyInterpreterRef -get_strong_ref(void) -{ - PyInterpreterRef ref; - if (PyInterpreterRef_Get(&ref) < 0) { - Py_FatalError("strong reference should not have failed"); - } - return ref; -} - #define NUM_REFS 100 static PyObject * From 531928eb2a586e33f1b5ae495afcc650e989089e Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 11:11:03 -0400 Subject: [PATCH 50/61] Add some more tests. --- Modules/_testcapimodule.c | 96 +++++++++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 18 deletions(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 66368828112611..0e7118363ecab7 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -14,6 +14,8 @@ #include "frameobject.h" // PyFrame_New() #include "marshal.h" // PyMarshal_WriteLongToFile() +#include "pylifecycle.h" +#include "pystate.h" #include // FLT_MAX #include @@ -2556,38 +2558,95 @@ get_strong_ref(void) return ref; } -static PyObject * -test_interp_ensure(PyObject *self, PyObject *unused) +static void +test_interp_ref_common(void) { PyInterpreterState *interp = PyInterpreterState_Get(); PyInterpreterRef ref = get_strong_ref(); + assert(PyInterpreterRef_AsInterpreter(ref) == interp); + + PyInterpreterRef ref_2 = PyInterpreterRef_Dup(ref); + assert(PyInterpreterRef_AsInterpreter(ref_2) == interp); + + // We can close the references in any order + PyInterpreterRef_Close(ref); + PyInterpreterRef_Close(ref_2); +} + +static PyObject * +test_interpreter_refs(PyObject *self, PyObject *unused) +{ + // Test the main interpreter + test_interp_ref_common(); + + // Test a (legacy) subinterpreter PyThreadState *save_tstate = PyThreadState_Swap(NULL); - PyThreadState *tstate = Py_NewInterpreter(); - PyInterpreterRef sub_ref = get_strong_ref(); - PyInterpreterState *subinterp = PyThreadState_GetInterpreter(tstate); + PyThreadState *interp_tstate = Py_NewInterpreter(); + test_interp_ref_common(); + Py_EndInterpreter(interp_tstate); + + // Test an isolated subinterpreter + PyInterpreterConfig config = { + .gil = PyInterpreterConfig_OWN_GIL, + .check_multi_interp_extensions = 1 + }; + + PyThreadState *isolated_interp_tstate; + PyStatus status = Py_NewInterpreterFromConfig(&isolated_interp_tstate, &config); + if (PyStatus_Exception(status)) { + PyErr_SetString(PyExc_RuntimeError, "interpreter creation failed"); + return NULL; + } + + test_interp_ref_common(); + Py_EndInterpreter(isolated_interp_tstate); + PyThreadState_Swap(save_tstate); + Py_RETURN_NONE; +} + +static PyObject * +test_thread_state_ensure_nested(PyObject *self, PyObject *unused) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + PyThreadState *save_tstate = PyThreadState_Swap(NULL); + assert(PyGILState_GetThisThreadState() == save_tstate); + PyInterpreterRef ref = get_strong_ref(); for (int i = 0; i < 10; ++i) { - int res = PyThreadState_Ensure(ref); - assert(res == 0); - assert(PyThreadState_GetUnchecked() != NULL); - assert(PyInterpreterState_Get() == interp); + // Test reactivation of the detached tstate. + if (PyThreadState_Ensure(ref) < 0) { + PyInterpreterRef_Close(ref); + return PyErr_NoMemory(); + } + + // No new thread state should've been created. + assert(PyThreadState_Get() == save_tstate); + PyThreadState_Release(); } + assert(PyThreadState_GetUnchecked() == NULL); + + // Similarly, test ensuring with deep nesting and *then* releasing. + // If the (detached) gilstate matches the interpreter, then it shouldn't + // create a new thread state. for (int i = 0; i < 10; ++i) { - int res = PyThreadState_Ensure(sub_ref); - assert(res == 0); - assert(PyInterpreterState_Get() == subinterp); + if (PyThreadState_Ensure(ref) < 0) { + // This will technically leak other thread states, but it doesn't + // matter because this is a test. + PyInterpreterRef_Close(ref); + return PyErr_NoMemory(); + } + + assert(PyThreadState_Get() == save_tstate); } - for (int i = 0; i < 20; ++i) { + for (int i = 0; i < 10; ++i) { + assert(PyThreadState_Get() == save_tstate); PyThreadState_Release(); } + assert(PyThreadState_GetUnchecked() == NULL); PyInterpreterRef_Close(ref); - PyInterpreterRef_Close(sub_ref); - - PyThreadState_Swap(save_tstate); - Py_RETURN_NONE; } static PyMethodDef TestMethods[] = { @@ -2684,7 +2743,8 @@ static PyMethodDef TestMethods[] = { {"test_atexit", test_atexit, METH_NOARGS}, {"code_offset_to_line", _PyCFunction_CAST(code_offset_to_line), METH_FASTCALL}, {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, - {"test_interp_ensure", test_interp_ensure, METH_NOARGS}, + {"test_interpreter_refs", test_interpreter_refs, METH_NOARGS}, + {"test_thread_state_ensure_nested", test_thread_state_ensure_nested, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; From 08a8af6257c1f9a1b0a22aa3d43a10196e187046 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 11:13:33 -0400 Subject: [PATCH 51/61] Remove unused variables. --- Modules/_testcapimodule.c | 2 +- Modules/_testinternalcapi.c | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 0e7118363ecab7..e4d548a76f63fc 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2607,7 +2607,6 @@ test_interpreter_refs(PyObject *self, PyObject *unused) static PyObject * test_thread_state_ensure_nested(PyObject *self, PyObject *unused) { - PyInterpreterState *interp = PyInterpreterState_Get(); PyThreadState *save_tstate = PyThreadState_Swap(NULL); assert(PyGILState_GetThisThreadState() == save_tstate); PyInterpreterRef ref = get_strong_ref(); @@ -2647,6 +2646,7 @@ test_thread_state_ensure_nested(PyObject *self, PyObject *unused) assert(PyThreadState_GetUnchecked() == NULL); PyInterpreterRef_Close(ref); + Py_RETURN_NONE; } static PyMethodDef TestMethods[] = { diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 35f9a56d226aea..d5f983b14c4d76 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2352,6 +2352,8 @@ test_interp_refcount(PyObject *self, PyObject *unused) PyInterpreterRef refs[NUM_REFS]; for (int i = 0; i < NUM_REFS; ++i) { int res = PyInterpreterRef_Get(&refs[i]); + (void)res; + assert(res == 0); assert(_PyInterpreterState_Refcount(interp) == i + 1); } @@ -2377,6 +2379,7 @@ test_interp_weakref_incref(PyObject *self, PyObject *unused) for (int i = 0; i < NUM_REFS; ++i) { int res = PyInterpreterWeakRef_AsStrong(wref, &refs[i]); + (void)res; assert(res == 0); assert(PyInterpreterRef_AsInterpreter(refs[i]) == interp); assert(_PyInterpreterState_Refcount(interp) == i + 1); From 02f93bccfe18a492e1f27718c061972f6cc24863 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 11:17:48 -0400 Subject: [PATCH 52/61] Fix some thread state attachment problems. --- Modules/_testcapimodule.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index e4d548a76f63fc..2a5d8146a5946c 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2607,9 +2607,9 @@ test_interpreter_refs(PyObject *self, PyObject *unused) static PyObject * test_thread_state_ensure_nested(PyObject *self, PyObject *unused) { + PyInterpreterRef ref = get_strong_ref(); PyThreadState *save_tstate = PyThreadState_Swap(NULL); assert(PyGILState_GetThisThreadState() == save_tstate); - PyInterpreterRef ref = get_strong_ref(); for (int i = 0; i < 10; ++i) { // Test reactivation of the detached tstate. @@ -2646,6 +2646,7 @@ test_thread_state_ensure_nested(PyObject *self, PyObject *unused) assert(PyThreadState_GetUnchecked() == NULL); PyInterpreterRef_Close(ref); + PyThreadState_Swap(save_tstate); Py_RETURN_NONE; } From d6c82bd7071f13649d618891b79f209f1610cf70 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 11:40:26 -0400 Subject: [PATCH 53/61] Only delete thread states created by PyThreadState_Ensure() --- Include/cpython/pystate.h | 16 ++++++++++++---- Python/pystate.c | 23 ++++++++++++++--------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 76793816af7fa9..abfe137e5905b4 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -207,11 +207,19 @@ struct _ts { PyObject *threading_local_sentinel; _PyRemoteDebuggerSupport remote_debugger_support; - /* Number of nested PyThreadState_Ensure() calls on this thread state */ - Py_ssize_t ensure_counter; + struct { + /* Number of nested PyThreadState_Ensure() calls on this thread state */ + Py_ssize_t counter; + + /* Thread state that was active before PyThreadState_Ensure() was called. */ + PyThreadState *prior_tstate; + + /* Should this thread state be deleted upon calling + PyThreadState_Release() (with the counter at 1)? - /* Thread state that was active before PyThreadState_Ensure() was called. */ - PyThreadState *prior_ensure; + This is only true for thread states created by PyThreadState_Ensure() */ + int delete_on_release; + } ensure; }; /* other API */ diff --git a/Python/pystate.c b/Python/pystate.c index 7c2078ecc61818..e31a58f0a36a84 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3320,7 +3320,7 @@ PyThreadState_Ensure(PyInterpreterRef interp_ref) PyThreadState *attached_tstate = current_fast_get(); if (attached_tstate != NULL && attached_tstate->interp == interp) { /* Yay! We already have an attached thread state that matches. */ - ++attached_tstate->ensure_counter; + ++attached_tstate->ensure.counter; return 0; } @@ -3328,7 +3328,7 @@ PyThreadState_Ensure(PyInterpreterRef interp_ref) if (detached_gilstate != NULL && detached_gilstate->interp == interp) { /* There's a detached thread state that works. */ assert(attached_tstate == NULL); - ++detached_gilstate->ensure_counter; + ++detached_gilstate->ensure.counter; _PyThreadState_Attach(detached_gilstate); return 0; } @@ -3338,10 +3338,11 @@ PyThreadState_Ensure(PyInterpreterRef interp_ref) if (fresh_tstate == NULL) { return -1; } - fresh_tstate->ensure_counter = 1; + fresh_tstate->ensure.counter = 1; + fresh_tstate->ensure.delete_on_release = 1; if (attached_tstate != NULL) { - fresh_tstate->prior_ensure = PyThreadState_Swap(fresh_tstate); + fresh_tstate->ensure.prior_tstate = PyThreadState_Swap(fresh_tstate); } else { _PyThreadState_Attach(fresh_tstate); } @@ -3354,15 +3355,19 @@ PyThreadState_Release(void) { PyThreadState *tstate = current_fast_get(); _Py_EnsureTstateNotNULL(tstate); - Py_ssize_t remaining = --tstate->ensure_counter; + Py_ssize_t remaining = --tstate->ensure.counter; if (remaining < 0) { Py_FatalError("PyThreadState_Release() called more times than PyThreadState_Ensure()"); } + PyThreadState *to_restore = tstate->ensure.prior_tstate; if (remaining == 0) { - PyThreadState *to_restore = tstate->prior_ensure; - PyThreadState_Clear(tstate); - PyThreadState_Swap(to_restore); - PyThreadState_Delete(tstate); + if (tstate->ensure.delete_on_release) { + PyThreadState_Clear(tstate); + PyThreadState_Swap(to_restore); + PyThreadState_Delete(tstate); + } else { + PyThreadState_Swap(to_restore); + } } return; From 082fd69b89bdba32c7b8547125c6c62f33fddc36 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 12:16:58 -0400 Subject: [PATCH 54/61] Add a test for crossinterpreter ensures. --- Modules/_testcapimodule.c | 53 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 2a5d8146a5946c..e2768d7d816bf6 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2650,6 +2650,58 @@ test_thread_state_ensure_nested(PyObject *self, PyObject *unused) Py_RETURN_NONE; } +static PyObject * +test_thread_state_ensure_crossinterp(PyObject *self, PyObject *unused) +{ + PyInterpreterRef ref = get_strong_ref(); + PyThreadState *save_tstate = PyThreadState_Swap(NULL); + PyThreadState *interp_tstate = Py_NewInterpreter(); + + /* This should create a new thread state for the calling interpreter, *not* + reactivate the old one. In a real-world scenario, this would arise in + something like this: + + def some_func(): + import something + # This re-enters the main interpreter, but we + # shouldn't have access to prior thread-locals. + something.call_something() + + interp = interpreters.create() + interp.exec(some_func) + */ + if (PyThreadState_Ensure(ref) < 0) { + PyInterpreterRef_Close(ref); + return PyErr_NoMemory(); + } + + PyThreadState *ensured_tstate = PyThreadState_Get(); + assert(ensured_tstate != save_tstate); + assert(PyInterpreterState_Get() == PyInterpreterRef_AsInterpreter(ref)); + assert(PyGILState_GetThisThreadState() == ensured_tstate); + + // Now though, we should reactivate the thread state + if (PyThreadState_Ensure(ref) < 0) { + PyInterpreterRef_Close(ref); + return PyErr_NoMemory(); + } + + assert(PyThreadState_Get() == ensured_tstate); + PyThreadState_Release(); + + // Ensure that we're restoring the prior thread state + PyThreadState_Release(); + assert(PyThreadState_Get() == interp_tstate); + assert(PyGILState_GetThisThreadState() == interp_tstate); + + PyThreadState_Swap(interp_tstate); + Py_EndInterpreter(interp_tstate); + + PyInterpreterRef_Close(ref); + PyThreadState_Swap(save_tstate); + Py_RETURN_NONE; +} + static PyMethodDef TestMethods[] = { {"set_errno", set_errno, METH_VARARGS}, {"test_config", test_config, METH_NOARGS}, @@ -2746,6 +2798,7 @@ static PyMethodDef TestMethods[] = { {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, {"test_interpreter_refs", test_interpreter_refs, METH_NOARGS}, {"test_thread_state_ensure_nested", test_thread_state_ensure_nested, METH_NOARGS}, + {"test_thread_state_ensure_crossinterp", test_thread_state_ensure_crossinterp, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; From 61f70aed0bd9c3efd1e02b1ac166e672d20f01a7 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 12:23:54 -0400 Subject: [PATCH 55/61] Add a test for weak interpreter references. --- Modules/_testcapimodule.c | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index e2768d7d816bf6..7aab3468e0e2e8 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -14,6 +14,7 @@ #include "frameobject.h" // PyFrame_New() #include "marshal.h" // PyMarshal_WriteLongToFile() +#include "pyerrors.h" #include "pylifecycle.h" #include "pystate.h" @@ -2656,6 +2657,10 @@ test_thread_state_ensure_crossinterp(PyObject *self, PyObject *unused) PyInterpreterRef ref = get_strong_ref(); PyThreadState *save_tstate = PyThreadState_Swap(NULL); PyThreadState *interp_tstate = Py_NewInterpreter(); + if (interp_tstate == NULL) { + PyInterpreterRef_Close(ref); + return PyErr_NoMemory(); + } /* This should create a new thread state for the calling interpreter, *not* reactivate the old one. In a real-world scenario, this would arise in @@ -2702,6 +2707,36 @@ test_thread_state_ensure_crossinterp(PyObject *self, PyObject *unused) Py_RETURN_NONE; } +static PyObject * +test_weak_interpreter_ref_after_shutdown(PyObject *self, PyObject *unused) +{ + PyThreadState *save_tstate = PyThreadState_Swap(NULL); + PyInterpreterWeakRef wref; + PyThreadState *interp_tstate = Py_NewInterpreter(); + if (interp_tstate == NULL) { + return PyErr_NoMemory(); + } + + int res = PyInterpreterWeakRef_Get(&wref); + (void)res; + assert(res == 0); + + // As a sanity check, ensure that the weakref actually works + PyInterpreterRef ref; + res = PyInterpreterWeakRef_AsStrong(wref, &ref); + assert(res == 0); + PyInterpreterRef_Close(ref); + + // Now, destroy the interpreter and try to acquire a weak reference. + // It should fail. + Py_EndInterpreter(interp_tstate); + res = PyInterpreterWeakRef_AsStrong(wref, &ref); + assert(res == -1); + + PyThreadState_Swap(save_tstate); + Py_RETURN_NONE; +} + static PyMethodDef TestMethods[] = { {"set_errno", set_errno, METH_VARARGS}, {"test_config", test_config, METH_NOARGS}, @@ -2799,6 +2834,7 @@ static PyMethodDef TestMethods[] = { {"test_interpreter_refs", test_interpreter_refs, METH_NOARGS}, {"test_thread_state_ensure_nested", test_thread_state_ensure_nested, METH_NOARGS}, {"test_thread_state_ensure_crossinterp", test_thread_state_ensure_crossinterp, METH_NOARGS}, + {"test_weak_interpreter_ref_after_shutdown", test_weak_interpreter_ref_after_shutdown, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; From fa961e9191e1014fe0338789dd2c750d3bbd0103 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 12:30:09 -0400 Subject: [PATCH 56/61] Fix concurrent shutdown races in PyGILState_Ensure(). --- Python/pystate.c | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Python/pystate.c b/Python/pystate.c index e31a58f0a36a84..9255e79dcfacaa 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -2759,30 +2759,24 @@ PyGILState_Check(void) PyGILState_STATE PyGILState_Ensure(void) { - _PyRuntimeState *runtime = &_PyRuntime; - /* Note that we do not auto-init Python here - apart from potential races with 2 threads auto-initializing, pep-311 spells out other issues. Embedders are expected to have called Py_Initialize(). */ - /* Ensure that _PyEval_InitThreads() and _PyGILState_Init() have been - called by Py_Initialize() - - TODO: This isn't thread-safe. There's no protection here against - concurrent finalization of the interpreter; it's simply a guard - for *after* the interpreter has finalized. - */ - if (!_PyEval_ThreadsInitialized() || runtime->gilstate.autoInterpreterState == NULL) { - PyThread_hang_thread(); - } - PyThreadState *tcur = gilstate_get(); int has_gil; + PyInterpreterRef ref; if (tcur == NULL) { /* Create a new Python thread state for this thread */ // XXX Use PyInterpreterState_EnsureThreadState()? - tcur = new_threadstate(runtime->gilstate.autoInterpreterState, + if (PyInterpreterRef_Main(&ref) < 0) { + // The main interpreter has finished, so we don't have + // any intepreter to make a thread state for. Hang the + // thread to act as failure. + PyThread_hang_thread(); + } + tcur = new_threadstate(PyInterpreterRef_AsInterpreter(ref), _PyThreadState_WHENCE_GILSTATE); if (tcur == NULL) { Py_FatalError("Couldn't create thread-state for new thread"); @@ -2804,6 +2798,10 @@ PyGILState_Ensure(void) PyEval_RestoreThread(tcur); } + if (tcur == NULL) { + PyInterpreterRef_Close(ref); + } + /* Update our counter in the thread-state - no need for locks: - tcur will remain valid as we hold the GIL. - the counter is safe as we are the only thread "allowed" From ea1da77fce1f14f42cf1e38ce4b95fbdf5dd8a8a Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 12:36:59 -0400 Subject: [PATCH 57/61] Add a test for PyInterpreterRef_Main(). --- Lib/test/test_embed.py | 3 +++ Programs/_testembed.c | 26 ++++++++++++++++++++++++++ Python/pystate.c | 6 ++++++ 3 files changed, 35 insertions(+) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 7e663c035b9a2e..124213b9ca36e6 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -1920,6 +1920,9 @@ def test_gilstate_after_finalization(self): def test_thread_state_ensure(self): self.run_embedded_interpreter("test_thread_state_ensure") + def test_main_interpreter_ref(self): + self.run_embedded_interpreter("test_main_interpreter_ref") + class MiscTests(EmbeddingTestsMixin, unittest.TestCase): def test_unicode_id_init(self): diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 7da334b54242dd..3d98e8f0b79d28 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2400,6 +2400,31 @@ test_gilstate_after_finalization(void) return PyThread_detach_thread(handle); } +static int +test_main_interpreter_ref(void) +{ + // It should not work before the runtime has started. + PyInterpreterRef ref; + int res = PyInterpreterRef_Main(&ref); + (void)res; + assert(res == -1); + + _testembed_initialize(); + + // Main interpreter is initialized and ready. + res = PyInterpreterRef_Main(&ref); + assert(res == 0); + assert(PyInterpreterRef_AsInterpreter(ref) == PyInterpreterState_Main()); + PyInterpreterRef_Close(ref); + + Py_Finalize(); + + // Main interpreter is dead, we can no longer acquire references to it. + res = PyInterpreterRef_Main(&ref); + assert(res == -1); + return 0; +} + /* ********************************************************* * List of test cases and the function that implements it. * @@ -2491,6 +2516,7 @@ static struct TestCase TestCases[] = { {"test_get_incomplete_frame", test_get_incomplete_frame}, {"test_thread_state_ensure", test_thread_state_ensure}, {"test_gilstate_after_finalization", test_gilstate_after_finalization}, + {"test_main_interpreter_ref", test_main_interpreter_ref}, {NULL, NULL} }; diff --git a/Python/pystate.c b/Python/pystate.c index 9255e79dcfacaa..95a74664a8948e 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3305,6 +3305,12 @@ PyInterpreterRef_Main(PyInterpreterRef *strong_ptr) assert(strong_ptr != NULL); _PyRuntimeState *runtime = &_PyRuntime; HEAD_LOCK(runtime); + if (runtime->initialized == 0) { + // Main interpreter is not initialized. + // This can be the case before Py_Initialize(), or after Py_Finalize(). + HEAD_UNLOCK(runtime); + return -1; + } int res = try_acquire_strong_ref(&runtime->_main_interpreter, strong_ptr); HEAD_UNLOCK(runtime); From 6f193841b778a931e8df5c0a3df1c05a1ddd5ece Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 12:53:27 -0400 Subject: [PATCH 58/61] Use PyErr_FormatUnraisable to show signals. --- Python/pylifecycle.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index f2714611611c74..a0a5617c2c39ad 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -37,6 +37,7 @@ #include "pycore_weakref.h" // _PyWeakref_GET_REF() #include "opcode.h" +#include "pyerrors.h" #include // setlocale() #include // getenv() @@ -3501,7 +3502,7 @@ wait_for_interp_references(PyInterpreterState *interp) wait_ns = Py_MIN(wait_ns, wait_max); if (PyErr_CheckSignals()) { - PyErr_Print(); + PyErr_FormatUnraisable("Exception ignored while waiting on interpreter shutdown"); /* The user CTRL+C'd us, bail out without waiting for a reference count of zero. From b702da2d22b19cd79c4e34e0650be2b87906a6b2 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 12:59:43 -0400 Subject: [PATCH 59/61] Fix a re-entrancy deadlock. --- Python/pystate.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Python/pystate.c b/Python/pystate.c index 95a74664a8948e..25782e78f8a074 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -2789,19 +2789,17 @@ PyGILState_Ensure(void) assert(tcur->gilstate_counter == 1); tcur->gilstate_counter = 0; has_gil = 0; /* new thread state is never current */ + PyInterpreterRef_Close(ref); } else { has_gil = holds_gil(tcur); } if (!has_gil) { + // XXX Do we need to protect this against finalization? PyEval_RestoreThread(tcur); } - if (tcur == NULL) { - PyInterpreterRef_Close(ref); - } - /* Update our counter in the thread-state - no need for locks: - tcur will remain valid as we hold the GIL. - the counter is safe as we are the only thread "allowed" From 40e7e68dad88082f02d1a95c935fc021213ea57a Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 1 Jun 2025 13:12:14 -0400 Subject: [PATCH 60/61] Remove stupid IDE imports. --- Modules/_testcapimodule.c | 3 --- Python/pylifecycle.c | 2 -- 2 files changed, 5 deletions(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 7aab3468e0e2e8..dad49b0f1ba8ea 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -14,9 +14,6 @@ #include "frameobject.h" // PyFrame_New() #include "marshal.h" // PyMarshal_WriteLongToFile() -#include "pyerrors.h" -#include "pylifecycle.h" -#include "pystate.h" #include // FLT_MAX #include diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index a0a5617c2c39ad..2eede55ff7115d 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -13,7 +13,6 @@ #include "pycore_freelist.h" // _PyObject_ClearFreeLists() #include "pycore_global_objects_fini_generated.h" // _PyStaticObjects_CheckRefcnt() #include "pycore_initconfig.h" // _PyStatus_OK() -#include "pycore_interp_structs.h" #include "pycore_interpolation.h" // _PyInterpolation_InitTypes() #include "pycore_long.h" // _PyLong_InitTypes() #include "pycore_object.h" // _PyDebug_PrintTotalRefs() @@ -37,7 +36,6 @@ #include "pycore_weakref.h" // _PyWeakref_GET_REF() #include "opcode.h" -#include "pyerrors.h" #include // setlocale() #include // getenv() From 2c52cdcb45e913f1ee1a9e55ffc4c5c2fa006cb1 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 21 Jun 2025 11:48:58 -0400 Subject: [PATCH 61/61] Fix interpreter reference count tests. --- Modules/_testinternalcapi.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index adc16dca931b4a..de0c179ae30b07 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2362,7 +2362,7 @@ test_interp_refcount(PyObject *self, PyObject *unused) for (int i = 0; i < NUM_REFS; ++i) { PyInterpreterRef_Close(refs[i]); - assert(_PyInterpreterState_Refcount(interp) == (NUM_REFS - i)); + assert(_PyInterpreterState_Refcount(interp) == (NUM_REFS - i - 1)); } Py_RETURN_NONE; @@ -2390,7 +2390,7 @@ test_interp_weakref_incref(PyObject *self, PyObject *unused) for (int i = 0; i < NUM_REFS; ++i) { PyInterpreterRef_Close(refs[i]); - assert(_PyInterpreterState_Refcount(interp) == (NUM_REFS - i)); + assert(_PyInterpreterState_Refcount(interp) == (NUM_REFS - i - 1)); } PyInterpreterWeakRef_Close(wref);