Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
1828b9a
Implement interpreter state reference counting.
ZeroIntensity Apr 24, 2025
d0895f9
Add a test for interpreter reference counts.
ZeroIntensity Apr 24, 2025
5c3ee8d
Add thread state daemon-ness that doesn't work yet.
ZeroIntensity Apr 25, 2025
7b9ac59
Add untested implementation of non-daemon native threads.
ZeroIntensity Apr 25, 2025
0868c15
Add a test for PyThreadState_SetDaemon().
ZeroIntensity Apr 25, 2025
e9ea644
Add untested implementation of Ensure()/Release() that probably doesn…
ZeroIntensity Apr 25, 2025
0ebdca4
Change some comments.
ZeroIntensity Apr 25, 2025
40989de
Add a test that I'm sure doesn't work.
ZeroIntensity Apr 25, 2025
d501f35
Use the interpreter's reference count and native thread countdown as …
ZeroIntensity Apr 25, 2025
c5ec89c
Fix the countdown decrement.
ZeroIntensity Apr 26, 2025
4e1f599
Remove unused variable.
ZeroIntensity Apr 26, 2025
3127a3f
Test for PyGILState_Ensure()
ZeroIntensity Apr 26, 2025
82b5b9f
Fix the test for the new reference counting.
ZeroIntensity Apr 26, 2025
fda9886
Add PyInterpreterState_Lookup()
ZeroIntensity Apr 26, 2025
f7723c0
Fix a few bugs and add a test.
ZeroIntensity Apr 26, 2025
62e9549
Add a test for PyThreadState_Ensure() across interpreters.
ZeroIntensity Apr 27, 2025
bc60630
Remove an artifact from old approach.
ZeroIntensity Apr 28, 2025
9d8d526
Fix test from earlier semantics.
ZeroIntensity Apr 28, 2025
54b0ce0
Remove 'daemonness' as a property of a thread.
ZeroIntensity May 22, 2025
5955de6
Add strong interpreter reference functions.
ZeroIntensity May 22, 2025
92cf906
Implement weak references.
ZeroIntensity May 22, 2025
b0d0673
Fix some thread safety issues regarding interpreter deletion.
ZeroIntensity May 22, 2025
0a15beb
Merge from main branch.
ZeroIntensity May 22, 2025
16d79de
Implement new version of PyThreadState_Ensure() and PyThreadState_Rel…
ZeroIntensity May 22, 2025
c2bffcd
Use the new APIs in the tests.
ZeroIntensity May 22, 2025
911c6b5
Fix _testcapi.
ZeroIntensity May 22, 2025
b84fa90
Fix the ensure counter.
ZeroIntensity May 22, 2025
9ccf6bd
Add the test to test_embed.
ZeroIntensity May 22, 2025
481caf5
Allow the wait to be interrupted by CTRL+C.
ZeroIntensity May 22, 2025
71e1aec
Print the error before bailing out.
ZeroIntensity May 22, 2025
d661578
Updates for the new proposal.
ZeroIntensity May 28, 2025
4249c5d
Bikeshedding.
ZeroIntensity May 28, 2025
71f2fd7
Apply suggestions from code review
ZeroIntensity May 28, 2025
03ccb38
Fix failing build.
ZeroIntensity May 29, 2025
bca65fb
Rename parameter.
ZeroIntensity May 29, 2025
510ade1
Fix formatting.
ZeroIntensity May 29, 2025
3971408
Add tstate check.
ZeroIntensity May 29, 2025
6c4c52b
Move to pycore_pystate.h
ZeroIntensity May 29, 2025
64920a8
Revert "Move to pycore_pystate.h"
ZeroIntensity May 29, 2025
05436f3
Use an exponential wait time for the event.
ZeroIntensity May 29, 2025
02bc2d7
Mark function as static.
ZeroIntensity May 29, 2025
03fa2af
Update fatal error message.
ZeroIntensity May 29, 2025
b955d85
Remove incorrect assertion.
ZeroIntensity May 29, 2025
5236700
Add a comment regarding PyMem_RawMalloc()
ZeroIntensity May 29, 2025
a277130
Add a comment.
ZeroIntensity Jun 1, 2025
dac0c1a
Update Include/cpython/pystate.h
ZeroIntensity Jun 1, 2025
ab9e3b5
Merge branch 'pep-788-impl' of https://github.com/ZeroIntensity/cpyth…
ZeroIntensity Jun 1, 2025
79a1852
Move some tests around to prevent exposure of the private API.
ZeroIntensity Jun 1, 2025
47957b8
Move weakref test to internal C API.
ZeroIntensity Jun 1, 2025
771d7ed
Improve reference counting tests.
ZeroIntensity Jun 1, 2025
0c3c1c7
Remove dead function.
ZeroIntensity Jun 1, 2025
531928e
Add some more tests.
ZeroIntensity Jun 1, 2025
08a8af6
Remove unused variables.
ZeroIntensity Jun 1, 2025
02f93bc
Fix some thread state attachment problems.
ZeroIntensity Jun 1, 2025
d6c82bd
Only delete thread states created by PyThreadState_Ensure()
ZeroIntensity Jun 1, 2025
082fd69
Add a test for crossinterpreter ensures.
ZeroIntensity Jun 1, 2025
61f70ae
Add a test for weak interpreter references.
ZeroIntensity Jun 1, 2025
fa961e9
Fix concurrent shutdown races in PyGILState_Ensure().
ZeroIntensity Jun 1, 2025
ea1da77
Add a test for PyInterpreterRef_Main().
ZeroIntensity Jun 1, 2025
6f19384
Use PyErr_FormatUnraisable to show signals.
ZeroIntensity Jun 1, 2025
b702da2
Fix a re-entrancy deadlock.
ZeroIntensity Jun 1, 2025
40e7e68
Remove stupid IDE imports.
ZeroIntensity Jun 1, 2025
96efc81
Merge branch 'main' of https://github.com/python/cpython into pep-788…
ZeroIntensity Jun 21, 2025
af6f6fd
Merge branch 'pep-788-impl' of https://github.com/ZeroIntensity/cpyth…
ZeroIntensity Jun 21, 2025
2c52cdc
Fix interpreter reference count tests.
ZeroIntensity Jun 21, 2025
3ffe9d0
Merge branch 'main' of https://github.com/python/cpython into pep-788…
ZeroIntensity Jul 9, 2025
588364c
Add thread state references to PyThreadState_Ensure() and
ZeroIntensity Jul 9, 2025
a1a332e
Merge branch 'main' of https://github.com/python/cpython into pep-788…
ZeroIntensity Sep 19, 2025
8000e9b
Merge branch 'main' of https://github.com/python/cpython into pep-788…
ZeroIntensity Sep 19, 2025
c3d09d2
Fix incorrect condition.
ZeroIntensity Sep 19, 2025
d1b3d80
Hand off the GIL when waiting on interpreter references.
ZeroIntensity Sep 19, 2025
f9b7040
Use new names from the PEP.
ZeroIntensity Sep 19, 2025
23ea059
Merge branch 'main' of https://github.com/python/cpython into pep-788…
ZeroIntensity Oct 4, 2025
cbd7254
Use the new names for the API.
ZeroIntensity Oct 4, 2025
a9da6b6
Update signatures for the new revision.
ZeroIntensity Oct 4, 2025
aaf3cef
Finish migration to the new API.
ZeroIntensity Oct 4, 2025
705d5cb
Fix failing tests.
ZeroIntensity Oct 4, 2025
2dcf975
Some touchups.
ZeroIntensity Oct 4, 2025
06520d2
Fix C analyzer.
ZeroIntensity Oct 4, 2025
51413fc
Merge branch 'main' of https://github.com/python/cpython into pep-788…
ZeroIntensity Oct 27, 2025
4371c15
Update to use new "PyInterpreterGuard" names.
ZeroIntensity Oct 27, 2025
0c2ce02
Fix some other missing cases.
ZeroIntensity Oct 27, 2025
2886803
Merge branch 'main' of https://github.com/python/cpython into pep-788…
ZeroIntensity Dec 7, 2025
9230fa2
Fix stray changes.
ZeroIntensity Dec 7, 2025
66e7978
Merge branch 'main' of https://github.com/python/cpython into pep-788…
ZeroIntensity Apr 19, 2026
ba4c538
Remove PyThreadView.
ZeroIntensity Apr 19, 2026
a767e43
Remove opaque pointer types.
ZeroIntensity Apr 19, 2026
b9d3bc8
Rename WHENCE_GILSTATE to WHENCE_C_API.
ZeroIntensity Apr 19, 2026
878803e
Add PyThreadState_EnsureFromView().
ZeroIntensity Apr 19, 2026
8bf1899
Fix compilation errors.
ZeroIntensity Apr 19, 2026
3320241
Use a dedicated heap allocation for interpreter guards.
ZeroIntensity Apr 20, 2026
7703906
Fix PyThreadState_EnsureFromView() and add a test.
ZeroIntensity Apr 21, 2026
19dfaca
Add a race test for PyThreadState_EnsureFromView.
ZeroIntensity Apr 21, 2026
23a84ab
Use an event for waiting on guards.
ZeroIntensity Apr 21, 2026
2e68b15
Emit a fatal error for CTRL^Cs while waiting on finalization guards.
ZeroIntensity Apr 21, 2026
331c0c9
Merge branch 'main' of https://github.com/python/cpython into pep-788…
ZeroIntensity Apr 21, 2026
829f96b
Close owned guards before deallocating the thread state, not after.
ZeroIntensity Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ struct _ts {
# define _PyThreadState_WHENCE_INIT 1
# define _PyThreadState_WHENCE_FINI 2
# define _PyThreadState_WHENCE_THREADING 3
# define _PyThreadState_WHENCE_GILSTATE 4
# define _PyThreadState_WHENCE_C_API 4
# define _PyThreadState_WHENCE_EXEC 5
# define _PyThreadState_WHENCE_THREADING_DAEMON 6
#endif
Expand Down Expand Up @@ -239,6 +239,20 @@ struct _ts {
// structure and all share the same per-interpreter structure).
PyStats *pystats;
#endif

struct {
/* Number of nested PyThreadState_Ensure() calls on this thread state */
Py_ssize_t counter;

/* Should this thread state be deleted upon calling
PyThreadState_Release() (with the counter at 1)?

This is only true for thread states created by PyThreadState_Ensure() */
int delete_on_release;

/* The interpreter guard owned by PyThreadState_EnsureFromView(), if any. */
PyInterpreterGuard *owned_guard;
} ensure;
};

/* other API */
Expand Down
6 changes: 6 additions & 0 deletions Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,12 @@ struct _is {
#endif
#endif

struct {
_PyRWMutex lock;
Py_ssize_t countdown;
PyEvent done;
} finalization_guards;

/* the initial PyInterpreterState.threads.head */
_PyThreadStateImpl _initial_thread;
// _initial_thread should be the last field of PyInterpreterState.
Expand Down
15 changes: 15 additions & 0 deletions Include/internal/pycore_pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,21 @@ _Py_RecursionLimit_GetMargin(PyThreadState *tstate)
#endif
}

/* PEP 788 structures. */

struct _PyInterpreterGuard {
PyInterpreterState *interp;
};

struct _PyInterpreterView {
int64_t id;
Py_ssize_t refcount;
};

// Exports for '_testinternalcapi' shared extension
PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_GuardCountdown(PyInterpreterState *interp);
PyAPI_FUNC(PyInterpreterState *) _PyInterpreterGuard_GetInterpreter(PyInterpreterGuard *guard);

#ifdef __cplusplus
}
#endif
Expand Down
17 changes: 17 additions & 0 deletions Include/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,23 @@ PyAPI_FUNC(void) PyGILState_Release(PyGILState_STATE);
PyAPI_FUNC(PyThreadState *) PyGILState_GetThisThreadState(void);


/* PEP 788 -- Interpreter guards and views. */

typedef struct _PyInterpreterGuard PyInterpreterGuard;
typedef struct _PyInterpreterView PyInterpreterView;

PyAPI_FUNC(PyInterpreterGuard *) PyInterpreterGuard_FromCurrent(void);
PyAPI_FUNC(void) PyInterpreterGuard_Close(PyInterpreterGuard *guard);
PyAPI_FUNC(PyInterpreterGuard *) PyInterpreterGuard_FromView(PyInterpreterView *view);

PyAPI_FUNC(PyInterpreterView *) PyInterpreterView_FromCurrent(void);
PyAPI_FUNC(void) PyInterpreterView_Close(PyInterpreterView *view);
PyAPI_FUNC(PyInterpreterView *) PyInterpreterView_FromMain(void);

PyAPI_FUNC(PyThreadState *) PyThreadState_Ensure(PyInterpreterGuard *guard);
PyAPI_FUNC(PyThreadState *) PyThreadState_EnsureFromView(PyInterpreterView *view);
PyAPI_FUNC(void) PyThreadState_Release(PyThreadState *tstate);

#ifndef Py_LIMITED_API
# define Py_CPYTHON_PYSTATE_H
# include "cpython/pystate.h"
Expand Down
10 changes: 9 additions & 1 deletion Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -1993,10 +1993,18 @@ 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")

def test_main_interpreter_view(self):
self.run_embedded_interpreter("test_main_interpreter_view")

def test_thread_state_ensure_from_view(self):
self.run_embedded_interpreter("test_thread_state_ensure_from_view")


class MiscTests(EmbeddingTestsMixin, unittest.TestCase):
def test_unicode_id_init(self):
Expand Down
239 changes: 239 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2606,6 +2606,240 @@ create_managed_weakref_nogc_type(PyObject *self, PyObject *Py_UNUSED(args))
return PyType_FromSpec(&ManagedWeakrefNoGC_spec);
}

static void
test_interp_guards_common(void)
{
PyInterpreterGuard *guard = PyInterpreterGuard_FromCurrent();
assert(guard != NULL);

PyInterpreterGuard *guard_2 = PyInterpreterGuard_FromCurrent();
assert(guard_2 != NULL);

// We can close the guards in any order
PyInterpreterGuard_Close(guard_2);
PyInterpreterGuard_Close(guard);
}

static PyObject *
test_interpreter_guards(PyObject *self, PyObject *unused)
{
// Test the main interpreter
test_interp_guards_common();

// Test a (legacy) subinterpreter
PyThreadState *save_tstate = PyThreadState_Swap(NULL);
PyThreadState *interp_tstate = Py_NewInterpreter();
test_interp_guards_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_guards_common();
Py_EndInterpreter(isolated_interp_tstate);
PyThreadState_Swap(save_tstate);
Py_RETURN_NONE;
}

static PyObject *
test_thread_state_ensure_nested(PyObject *self, PyObject *unused)
{
PyInterpreterGuard *guard = PyInterpreterGuard_FromCurrent();
if (guard == NULL) {
return NULL;
}
PyThreadState *save_tstate = PyThreadState_Swap(NULL);
assert(PyGILState_GetThisThreadState() == save_tstate);
PyThreadState *thread_states[10];

for (int i = 0; i < 10; ++i) {
// Test reactivation of the detached tstate.
thread_states[i] = PyThreadState_Ensure(guard);
if (thread_states[i] == 0) {
PyInterpreterGuard_Close(guard);
return PyErr_NoMemory();
}

// No new thread state should've been created.
assert(PyThreadState_Get() == save_tstate);
PyThreadState_Release(thread_states[i]);
}

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) {
thread_states[i] = PyThreadState_Ensure(guard);
if (thread_states[i] == 0) {
// This will technically leak other thread states, but it doesn't
// matter because this is a test.
PyInterpreterGuard_Close(guard);
return PyErr_NoMemory();
}

assert(PyThreadState_Get() == save_tstate);
}

for (int i = 0; i < 10; ++i) {
assert(PyThreadState_Get() == save_tstate);
PyThreadState_Release(thread_states[i]);
}
Comment thread
vstinner marked this conversation as resolved.

assert(PyThreadState_GetUnchecked() == NULL);
PyInterpreterGuard_Close(guard);
PyThreadState_Swap(save_tstate);
Py_RETURN_NONE;
}

static PyObject *
test_thread_state_ensure_crossinterp(PyObject *self, PyObject *unused)
{
PyInterpreterGuard *guard = PyInterpreterGuard_FromCurrent();
PyThreadState *save_tstate = PyThreadState_Swap(NULL);
PyThreadState *interp_tstate = Py_NewInterpreter();
assert(interp_tstate != NULL);

/* 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)
*/
PyThreadState *thread_state = PyThreadState_Ensure(guard);
assert(thread_state != NULL);

PyThreadState *ensured_tstate = PyThreadState_Get();
assert(ensured_tstate != save_tstate);
assert(PyGILState_GetThisThreadState() == ensured_tstate);

// Now though, we should reactivate the thread state
PyThreadState *other_thread_state = PyThreadState_Ensure(guard);
assert(other_thread_state != NULL);
assert(PyThreadState_Get() == ensured_tstate);

PyThreadState_Release(other_thread_state);

// Ensure that we're restoring the prior thread state
PyThreadState_Release(thread_state);
assert(PyThreadState_Get() == interp_tstate);
assert(PyGILState_GetThisThreadState() == interp_tstate);

PyThreadState_Swap(interp_tstate);
Py_EndInterpreter(interp_tstate);

PyInterpreterGuard_Close(guard);
PyThreadState_Swap(save_tstate);
Py_RETURN_NONE;
}

static PyObject *
test_interp_view_after_shutdown(PyObject *self, PyObject *unused)
{
PyThreadState *save_tstate = PyThreadState_Swap(NULL);
PyThreadState *interp_tstate = Py_NewInterpreter();
if (interp_tstate == NULL) {
PyThreadState_Swap(save_tstate);
return PyErr_NoMemory();
}

PyInterpreterView *view = PyInterpreterView_FromCurrent();
if (view == NULL) {
Py_EndInterpreter(interp_tstate);
PyThreadState_Swap(save_tstate);
return PyErr_NoMemory();
}

// As a sanity check, ensure that the view actually works
PyInterpreterGuard *guard = PyInterpreterGuard_FromView(view);
PyInterpreterGuard_Close(guard);

// Now, destroy the interpreter and try to acquire a lock from a view.
// It should fail.
Py_EndInterpreter(interp_tstate);
guard = PyInterpreterGuard_FromView(view);
assert(guard == NULL);

PyThreadState_Swap(save_tstate);
Py_RETURN_NONE;
}

static PyObject *
test_thread_state_ensure_view(PyObject *self, PyObject *unused)
{
// For simplicity's sake, we assume that functions won't fail due to being
// out of memory.
PyThreadState *save_tstate = PyThreadState_Swap(NULL);
PyThreadState *interp_tstate = Py_NewInterpreter();
assert(interp_tstate != NULL);
assert(PyInterpreterState_Get() == PyThreadState_GetInterpreter(interp_tstate));

PyInterpreterView *main_view = PyInterpreterView_FromMain();
assert(main_view != NULL);

PyInterpreterView *view = PyInterpreterView_FromCurrent();
assert(view != NULL);

Py_BEGIN_ALLOW_THREADS;
PyThreadState *tstate = PyThreadState_EnsureFromView(view);
assert(tstate != NULL);
assert(PyThreadState_Get() == interp_tstate);

// Test a nested call
PyThreadState *tstate2 = PyThreadState_EnsureFromView(view);
assert(PyThreadState_Get() == interp_tstate);

// We're in a new interpreter now. PyThreadState_EnsureFromView() should
// now create a new thread state.
PyThreadState *main_tstate = PyThreadState_EnsureFromView(main_view);
assert(main_tstate == interp_tstate); // The old thread state
assert(PyInterpreterState_Get() == PyInterpreterState_Main());

// Going back to the old interpreter should create a new thread state again.
PyThreadState *tstate3 = PyThreadState_EnsureFromView(view);
assert(PyInterpreterState_Get() == PyThreadState_GetInterpreter(interp_tstate));
assert(PyThreadState_Get() != interp_tstate);
PyThreadState_Release(tstate3);
PyThreadState_Release(main_tstate);

// We're back in the original interpreter. PyThreadState_EnsureFromView() should
// no longer create a new thread state.
assert(PyThreadState_Get() == interp_tstate);
PyThreadState *tstate4 = PyThreadState_EnsureFromView(view);
assert(PyThreadState_Get() == interp_tstate);
PyThreadState_Release(tstate4);
PyThreadState_Release(tstate2);
PyThreadState_Release(tstate);
assert(PyThreadState_GetUnchecked() == NULL);
Py_END_ALLOW_THREADS;

assert(PyThreadState_Get() == interp_tstate);
PyInterpreterView_Close(view);
PyInterpreterView_Close(main_view);
Py_EndInterpreter(interp_tstate);
PyThreadState_Swap(save_tstate);

Py_RETURN_NONE;
}


static PyObject*
test_soft_deprecated_macros(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args))
Expand Down Expand Up @@ -2740,6 +2974,11 @@ static PyMethodDef TestMethods[] = {
{"create_managed_weakref_nogc_type",
create_managed_weakref_nogc_type, METH_NOARGS},
{"test_soft_deprecated_macros", test_soft_deprecated_macros, METH_NOARGS},
{"test_interpreter_lock", test_interpreter_guards, 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_interp_view_after_shutdown", test_interp_view_after_shutdown, METH_NOARGS},
{"test_thread_state_ensure_view", test_thread_state_ensure_view, METH_NOARGS},
{NULL, NULL} /* sentinel */
};

Expand Down
Loading
Loading