From 25a3f52ab678f99f57deb5b9ac16cffa13f2e625 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 3 Jul 2025 10:25:52 -0400 Subject: [PATCH 1/8] Add a thread reference parameter to PyThreadState_Ensure() and PyThreadState_Release() --- peps/pep-0788.rst | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/peps/pep-0788.rst b/peps/pep-0788.rst index 41cc45f1a8c..f71c293c36d 100644 --- a/peps/pep-0788.rst +++ b/peps/pep-0788.rst @@ -563,7 +563,16 @@ Ensuring and Releasing Thread States This proposal includes two new high-level threading APIs that intend to replace :c:func:`PyGILState_Ensure` and :c:func:`PyGILState_Release`. -.. c:function:: int PyThreadState_Ensure(PyInterpreterRef ref) +.. c:type:: PyThreadRef + + An opaque reference to a :c:type:`PyThreadState`. + + As of now, the interpreter will do nothing different for thread references, + but this may change in the future. + + This type is guaranteed to be pointer-sized. + +.. c:function:: int PyThreadState_Ensure(PyInterpreterRef ref, PyThreadRef *thread) Ensure that the thread has an :term:`attached thread state` for the interpreter denoted by *ref*, and thus can safely invoke that @@ -580,9 +589,12 @@ replace :c:func:`PyGILState_Ensure` and :c:func:`PyGILState_Release`. if the interpreter matches *ref*, it is attached, and otherwise a new thread state is created. - Return ``0`` on success, and ``-1`` on failure. + The old thread state is stored as a thread reference in *\*thread*, and is + to be restored by :c:func:`PyThreadState_Release`. + + Return ``0`` on success, and ``-1`` without an exception set on failure. -.. c:function:: void PyThreadState_Release() +.. c:function:: void PyThreadState_Release(PyThreadRef ref) Release a :c:func:`PyThreadState_Ensure` call. From b3451e3f16ad2832f45e069b209e02665a7398df Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 3 Jul 2025 10:30:54 -0400 Subject: [PATCH 2/8] Remove usage of the term 'native thread'. --- peps/pep-0788.rst | 56 +++++++++++++---------------------------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/peps/pep-0788.rst b/peps/pep-0788.rst index f71c293c36d..e5a50df6a35 100644 --- a/peps/pep-0788.rst +++ b/peps/pep-0788.rst @@ -1,5 +1,5 @@ PEP: 788 -Title: Reimagining Native Threads +Title: Attaching Thread States via Interpreter References Author: Peter Bierma Sponsor: Victor Stinner Discussions-To: https://discuss.python.org/t/93653 @@ -49,37 +49,11 @@ this in CPython is :c:func:`PyGILState_Ensure`. As part of this proposal, :c:func:`PyThreadState_Ensure` is provided as a modern replacement that takes a strong interpreter reference. -Terminology -=========== - -Interpreters ------------- - -In this proposal, "interpreter" refers to a singular, isolated interpreter -(see :pep:`684`), with its own :c:type:`PyInterpreterState` pointer (referred -to as an "interpreter-state"). "Interpreter" *does not* refer to the entirety -of a Python process. - -The "current interpreter" refers to the interpreter-state -pointer on an :term:`attached thread state`, as returned by -:c:func:`PyThreadState_GetInterpreter` or :c:func:`PyInterpreterState_Get`. - -Native and Python Threads -------------------------- - -This PEP refers to a thread created using the C API as a "native thread", -also sometimes referred to as a "non-Python created thread", where a "Python -created" is a thread created by the :mod:`threading` module. - -A native thread is typically registered with the interpreter by -:c:func:`PyGILState_Ensure`, but any thread with an :term:`attached thread state` -qualifies as a native thread. - Motivation ========== -Native Threads Always Hang During Finalization ----------------------------------------------- +Non-Python Threads Always Hang During Finalization +-------------------------------------------------- Many large libraries might need to call Python code in highly-asynchronous situations where the desired interpreter @@ -111,7 +85,7 @@ Generally, this pattern would look something like this: /* ... */ } -In the current C API, any "native" thread (one not created via the +In the current C API, any non-Python thread (one not created via the :mod:`threading` module) is considered to be "daemon", meaning that the interpreter won't wait on that thread before shutting down. Instead, the interpreter will hang the thread when it goes to :term:`attach ` a :term:`thread state`, @@ -123,7 +97,7 @@ interpreter is finalizing isn't enough to safely call Python code. (Note that ha the thread is relatively new behavior; in prior versions, the thread would exit, but the issue is the same.) -This means that any non-Python/native thread may be terminated at any point, which +This means that any non-Python thread may be terminated at any point, which is severely limiting for users who want to do more than just execute Python code in their stream of calls. @@ -219,7 +193,7 @@ Joining the Thread isn't Always a Good Idea ******************************************* Even in daemon threads, it's generally *possible* to prevent hanging of -native threads through :mod:`atexit` functions. +non-Python threads through :mod:`atexit` functions. A thread could be started by some C function, and then as long as that thread is joined by :mod:`atexit`, then the thread won't hang. @@ -332,13 +306,13 @@ at the same time, causing a data race. An Interpreter Can Concurrently Deallocate ------------------------------------------ -The other way of creating a native thread that can invoke Python, -:c:func:`PyThreadState_New` and :c:func:`PyThreadState_Swap`, is a lot better -for supporting subinterpreters (because :c:func:`PyThreadState_New` takes an -explicit interpreter, rather than assuming that the main interpreter was -requested), but is still limited by the current hanging problems in the C API. -Manual creation of thread states ("manual" in contrast to the implicit creation -of one in :c:func:`PyGILState_Ensure`) does not solve any of the aforementioned +The other way of creating a non-Python thread, :c:func:`PyThreadState_New` and +:c:func:`PyThreadState_Swap`, is a lot better for supporting subinterpreters +(because :c:func:`PyThreadState_New` takes an explicit interpreter, rather than +assuming that the main interpreter was requested), but is still limited by the +current hanging problems in the C API. Manual creation of thread states +("manual" in contrast to the implicit creation of one in +:c:func:`PyGILState_Ensure`) does not solve any of the aforementioned thread-safety issues with thread states. In addition, subinterpreters typically have a much shorter lifetime than the @@ -819,7 +793,7 @@ This is the same code, rewritten to use the new functions: Example: A Daemon Thread ************************ -With this PEP, daemon threads are very similar to how native threads are used +With this PEP, daemon threads are very similar to how non-Python threads work in the C API today. After calling :c:func:`PyThreadState_Ensure`, simply release the interpreter reference, allowing the interpreter to shut down. @@ -1027,7 +1001,7 @@ of requiring less magic: on 32-bit systems, where ``void *`` is too small for an ``int64_t``. - To retain usability, interpreter ID APIs would still need to keep a reference count, otherwise the interpreter could be finalizing before - the native thread gets a chance to attach. The problem with using an + the non-Python thread gets a chance to attach. The problem with using an interpreter ID is that the reference count has to be "invisible"; it must be tracked elsewhere in the interpreter, likely being *more* complex than :c:func:`PyInterpreterRef_Get`. There's also a lack From 803b58f9fc45c4ab83a73615890d2a8c61091f17 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 3 Jul 2025 10:34:42 -0400 Subject: [PATCH 3/8] Rename the PEP. --- peps/pep-0788.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0788.rst b/peps/pep-0788.rst index e5a50df6a35..232ed970d0f 100644 --- a/peps/pep-0788.rst +++ b/peps/pep-0788.rst @@ -1,5 +1,5 @@ PEP: 788 -Title: Attaching Thread States via Interpreter References +Title: Interpreter References Author: Peter Bierma Sponsor: Victor Stinner Discussions-To: https://discuss.python.org/t/93653 From 6491193bd63f5ae358537978d8aaa905d4ebb198 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 3 Jul 2025 10:38:57 -0400 Subject: [PATCH 4/8] Update the examples to use a PyThreadRef parameter. --- peps/pep-0788.rst | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/peps/pep-0788.rst b/peps/pep-0788.rst index 232ed970d0f..f455861a2e7 100644 --- a/peps/pep-0788.rst +++ b/peps/pep-0788.rst @@ -654,7 +654,8 @@ With this PEP, you'd implement it like this: return -1; } - if (PyThreadState_Ensure(ref) < 0) { + PyThreadRef thread; + if (PyThreadState_Ensure(ref, &thread) < 0) { PyInterpreterRef_Close(ref); puts("Out of memory.\n", stderr); return -1; @@ -665,7 +666,7 @@ With this PEP, you'd implement it like this: free(to_write); PyErr_Print(); - PyThreadState_Release(); + PyThreadState_Release(thread); PyInterpreterRef_Close(ref); return res < 0; } @@ -756,14 +757,15 @@ This is the same code, rewritten to use the new functions: thread_func(void *arg) { PyInterpreterRef interp = (PyInterpreterRef)arg; - if (PyThreadState_Ensure(interp) < 0) { + PyThreadRef thread; + if (PyThreadState_Ensure(interp, &thread) < 0) { PyInterpreterRef_Close(interp); return -1; } if (PyRun_SimpleString("print(42)") < 0) { PyErr_Print(); } - PyThreadState_Release(); + PyThreadState_Release(&thread); PyInterpreterRef_Close(interp); return 0; } @@ -803,7 +805,8 @@ release the interpreter reference, allowing the interpreter to shut down. thread_func(void *arg) { PyInterpreterRef ref = (PyInterpreterRef)arg; - if (PyThreadState_Ensure(ref) < 0) { + PyThreadRef thread; + if (PyThreadState_Ensure(ref, &thread) < 0) { PyInterpreterRef_Close(ref); return -1; } @@ -813,7 +816,7 @@ release the interpreter reference, allowing the interpreter to shut down. if (PyRun_SimpleString("print(42)") < 0) { PyErr_Print(); } - PyThreadState_Release(); + PyThreadState_Release(thread); return 0; } @@ -859,14 +862,15 @@ deadlock the interpreter if it's not released. return -1; } - if (PyThreadState_Ensure(ref) < 0) { + PyThreadRef thread; + if (PyThreadState_Ensure(ref, &thread) < 0) { PyInterpreterRef_Close(ref); return -1; } if (PyRun_SimpleString("print(42)") < 0) { PyErr_Print(); } - PyThreadState_Release(); + PyThreadState_Release(thread); PyInterpreterRef_Close(ref); return 0; } @@ -917,14 +921,15 @@ interpreter here. return; } - if (PyThreadState_Ensure(ref) < 0) { + PyThreadRef thread; + if (PyThreadState_Ensure(ref, &thread) < 0) { PyInterpreterRef_Close(ref); return -1; } if (PyRun_SimpleString("print(42)") < 0) { PyErr_Print(); } - PyThreadState_Release(); + PyThreadState_Release(thread); PyInterpreterRef_Close(ref); return 0; } From 01f842215311bebdca5b970bf61acbcc7d75066f Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 4 Jul 2025 11:00:55 -0400 Subject: [PATCH 5/8] Remove references to old title in the abstract. --- peps/pep-0788.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/peps/pep-0788.rst b/peps/pep-0788.rst index f455861a2e7..855eb43ae80 100644 --- a/peps/pep-0788.rst +++ b/peps/pep-0788.rst @@ -32,10 +32,9 @@ inside of subinterpreters, primarily because :c:func:`PyGILState_Ensure` always creates a thread state for the main interpreter in threads where Python hasn't ever run. -This PEP intends to solve these kinds issues by *reimagining* how we approach -thread states in the C API. This is done through the introduction of interpreter -references that prevent an interpreter from finalizing (or more technically, -entering a stage in which attachment of a thread state hangs). +This PEP intends to solve these kinds issues through the introduction of +interpreter references that prevent an interpreter from finalizing (or more +technically, entering a stage in which attachment of a thread state hangs). This allows for more structure and reliability when it comes to thread state management, because it forces a layer of synchronization between the interpreter and the caller. From 2438c5fd907f0e343c3fc63747f6479304977a67 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 4 Jul 2025 11:03:25 -0400 Subject: [PATCH 6/8] Avoid usage of the name 'thread' in examples. For whatever reason, the rendered highlighter treats these as keywords. --- peps/pep-0788.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/peps/pep-0788.rst b/peps/pep-0788.rst index 855eb43ae80..3e4bef8103c 100644 --- a/peps/pep-0788.rst +++ b/peps/pep-0788.rst @@ -653,8 +653,8 @@ With this PEP, you'd implement it like this: return -1; } - PyThreadRef thread; - if (PyThreadState_Ensure(ref, &thread) < 0) { + PyThreadRef thread_ref; + if (PyThreadState_Ensure(ref, &thread_ref) < 0) { PyInterpreterRef_Close(ref); puts("Out of memory.\n", stderr); return -1; @@ -665,7 +665,7 @@ With this PEP, you'd implement it like this: free(to_write); PyErr_Print(); - PyThreadState_Release(thread); + PyThreadState_Release(thread_ref); PyInterpreterRef_Close(ref); return res < 0; } @@ -756,15 +756,15 @@ This is the same code, rewritten to use the new functions: thread_func(void *arg) { PyInterpreterRef interp = (PyInterpreterRef)arg; - PyThreadRef thread; - if (PyThreadState_Ensure(interp, &thread) < 0) { + PyThreadRef thread_ref; + if (PyThreadState_Ensure(interp, &thread_ref) < 0) { PyInterpreterRef_Close(interp); return -1; } if (PyRun_SimpleString("print(42)") < 0) { PyErr_Print(); } - PyThreadState_Release(&thread); + PyThreadState_Release(thread_ref); PyInterpreterRef_Close(interp); return 0; } @@ -804,8 +804,8 @@ release the interpreter reference, allowing the interpreter to shut down. thread_func(void *arg) { PyInterpreterRef ref = (PyInterpreterRef)arg; - PyThreadRef thread; - if (PyThreadState_Ensure(ref, &thread) < 0) { + PyThreadRef thread_ref; + if (PyThreadState_Ensure(ref, &thread_ref) < 0) { PyInterpreterRef_Close(ref); return -1; } @@ -815,7 +815,7 @@ release the interpreter reference, allowing the interpreter to shut down. if (PyRun_SimpleString("print(42)") < 0) { PyErr_Print(); } - PyThreadState_Release(thread); + PyThreadState_Release(thread_ref); return 0; } @@ -861,15 +861,15 @@ deadlock the interpreter if it's not released. return -1; } - PyThreadRef thread; - if (PyThreadState_Ensure(ref, &thread) < 0) { + PyThreadRef thread_ref; + if (PyThreadState_Ensure(ref, &thread_ref) < 0) { PyInterpreterRef_Close(ref); return -1; } if (PyRun_SimpleString("print(42)") < 0) { PyErr_Print(); } - PyThreadState_Release(thread); + PyThreadState_Release(thread_ref); PyInterpreterRef_Close(ref); return 0; } @@ -920,15 +920,15 @@ interpreter here. return; } - PyThreadRef thread; - if (PyThreadState_Ensure(ref, &thread) < 0) { + PyThreadRef thread_ref; + if (PyThreadState_Ensure(ref, &thread_ref) < 0) { PyInterpreterRef_Close(ref); return -1; } if (PyRun_SimpleString("print(42)") < 0) { PyErr_Print(); } - PyThreadState_Release(thread); + PyThreadState_Release(thread_ref); PyInterpreterRef_Close(ref); return 0; } From 7e300554d5a3e3fa1b0a4958b564ddb0279cfc8f Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 8 Jul 2025 20:18:39 -0400 Subject: [PATCH 7/8] Use the 'thread state' term rather than a 'PyThreadState' reference. --- peps/pep-0788.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0788.rst b/peps/pep-0788.rst index 3e4bef8103c..a35eecbe7e4 100644 --- a/peps/pep-0788.rst +++ b/peps/pep-0788.rst @@ -538,7 +538,7 @@ replace :c:func:`PyGILState_Ensure` and :c:func:`PyGILState_Release`. .. c:type:: PyThreadRef - An opaque reference to a :c:type:`PyThreadState`. + An opaque reference to a :term:`thread state`. As of now, the interpreter will do nothing different for thread references, but this may change in the future. From 231ba90bf897c4779fe6b5f048e20cb269cd8ae0 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 9 Jul 2025 08:55:48 -0400 Subject: [PATCH 8/8] Update peps/pep-0788.rst Co-authored-by: Petr Viktorin --- peps/pep-0788.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/peps/pep-0788.rst b/peps/pep-0788.rst index a35eecbe7e4..812cd080ab2 100644 --- a/peps/pep-0788.rst +++ b/peps/pep-0788.rst @@ -540,8 +540,9 @@ replace :c:func:`PyGILState_Ensure` and :c:func:`PyGILState_Release`. An opaque reference to a :term:`thread state`. - As of now, the interpreter will do nothing different for thread references, - but this may change in the future. + In the initial implementation, holding a thread reference will + not block finalization of threads or interpreters. + This may change in the future. This type is guaranteed to be pointer-sized.