8000 Implement PEP 788 by ZeroIntensity · Pull Request #133110 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

Implement PEP 788 #133110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 67 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
67 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 8000 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
8000 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
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
48 changes: 48 additions & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ 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;

/* Thread state that was active before PyThreadState_Ensure() was called. */
PyThreadState *prior_ensure;
};

/* other API */
Expand Down Expand Up @@ -259,3 +265,45 @@ PyAPI_FUNC(_PyFrameEvalFunction) _PyInterpreterState_GetEvalFrameFunc(
PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameFunc(
PyInterpreterState *interp,
_PyFrameEvalFunction eval_frame);

/* Strong interpreter references */

typedef uintptr_t PyInterpreterRef;

PyAPI_FUNC(int) PyInterpreterRef_Get(PyInterpreterRef *ptr);
PyAPI_FUNC(PyInterpreterRef) PyInterpreterRef_Dup(PyInterpreterRef ref);
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)

/* Weak interpreter references */

typedef struct _PyInterpreterWeakRef {
int64_t id;
Py_ssize_t refcount;
} _PyInterpreterWeakRef;

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move it to the internal C API? (Include/internal/pycore_pystate.h).

Same remark for _PyInterpreterState_Incref().

Copy link
Member Author
@ZeroIntensity ZeroIntensity May 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_testcapi doesn't allow importing from pycore headers. I only need these for those tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move tests to _testinternalcapi in this case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we're not testing the internal C API, we're testing the public C API. We just use the internal C API to write those tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't add private functions (ex: _PyInterpreterState_Refcount()) to the public C API. So move it to the internal C API. Sadly, it means moving your tests to the internal C API tests, unless a test only uses public functions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I split up the tests, so now the internal stuff is in _testinternalcapi and the internal functions are in the private headers.

PyAPI_FUNC(int) _PyInterpreterState_Incref(PyInterpreterState *interp);

PyAPI_FUNC(int) PyThreadState_Ensure(PyInterpreterRef interp_ref);

PyAPI_FUNC(void) PyThreadState_Release(void);
7 changes: 7 additions & 0 deletions Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,13 @@ 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;
int shutting_down;
} finalizing;
} threads;

/* Reference to the _PyRuntime global variable. This field exists
Expand Down
4 changes: 3 additions & 1 deletion Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -1914,10 +1914,12 @@ 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")


class MiscTests(EmbeddingTestsMixin, unittest.TestCase):
def test_unicode_id_init(self):
Expand Down
98 changes: 98 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2546,6 +2546,101 @@ 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)
{
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)
{
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 PyObject *
test_interp_ensure(PyObject *self, PyObject *unused)
{
PyInterpreterState *interp = PyInterpreterState_Get();
PyInterpreterRef ref = get_strong_ref();
PyThreadState *save_tstate = PyThreadState_Swap(NULL);
PyThreadState *tstate = Py_NewInterpreter();
PyInterpreterRef sub_ref = get_strong_ref();
PyInterpreterState *subinterp = PyThreadState_GetInterpreter(tstate);

for (int i = 0; i < 10; ++i) {
int res = PyThreadState_Ensure(ref);
assert(res == 0);
assert(PyThreadState_GetUnchecked() != NULL);
assert(PyInterpreterState_Get() == interp);
}

for (int i = 0; i < 10; ++i) {
int res = PyThreadState_Ensure(sub_ref);
assert(res == 0);
assert(PyInterpreterState_Get() == subinterp);
}

for (int i = 0; i < 20; ++i) {
PyThreadState_Release();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose that PyThreadState_GetUnchecked() should be NULL at this point? Maybe add a check for that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I redid this test.


PyInterpreterRef_Close(ref);
PyInterpreterRef_Close(sub_ref);

PyThreadState_Swap(save_tstate);
Py_RETURN_NONE;
}

static PyMethodDef TestMethods[] = {
{"set_errno", set_errno, METH_VARARGS},
{"test_config", test_config, METH_NOARGS},
Expand Down Expand Up @@ -2640,6 +2735,9 @@ 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 */
};

Expand Down
58 changes: 58 additions & 0 deletions Programs/_testembed.c
Original file line number Diff line number Diff line change
Expand Up @@ -2313,6 +2313,63 @@ 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)";

typedef struct {
PyInterpreterRef ref;
int done;
} ThreadData;

static void
do_tstate_ensure(void *arg)
{
ThreadData *data = (ThreadData *)arg;
int res = PyThreadState_Ensure(data->ref);
assert(res == 0);
PyThreadState_Ensure(data->ref);
PyThreadState_Ensure(data->ref);
PyGILState_STATE gstate = PyGILState_Ensure();
PyThreadState_Ensure(data->ref);
res = PyRun_SimpleString(THREAD_CODE);
PyThreadState_Release();
PyGILState_Release(gstate);
PyThreadState_Release();
PyThreadState_Release();
assert(res == 0);
PyThreadState_Release();
PyInterpreterRef_Close(data->ref);
data->done = 1;
}

static int
test_thread_state_ensure(void)
{
_testembed_initialize();
PyThread_handle_t handle;
PyThread_ident_t ident;
PyInterpreterRef ref;
if (PyInterpreterRef_Get(&ref) < 0) {
return -1;
};
ThreadData data = { ref };
if (PyThread_start_joinable_thread(do_tstate_ensure, &data,
&ident, &handle) < 0) {
PyInterpreterRef_Close(ref);
return -1;
}
Py_Finalize();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be safe to add a PyEvent to wait until the thread started before calling Py_Finalize().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We hold a strong interpreter reference, we're implicitly waiting for it to start anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh you're right, but I always forget the subtle semantics of interpreter strong references. Can you please add a comment explaining that the interpreter reference makes sure that Py_Finalize() doesn't complete until PyInterpreterRef_Close() is called? Or something like that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

assert(data.done == 1);
return 0;
}

static void
do_gilstate_ensure(void *event_ptr)
{
Expand Down Expand Up @@ -2429,6 +2486,7 @@ static struct TestCase TestCases[] = {
{"test_frozenmain", test_frozenmain},
#endif
{"test_get_incomplete_frame", test_get_incomplete_frame},
{"test_thread_state_ensure", test_thread_state_ensure},
{"test_gilstate_after_finalization", test_gilstate_after_finalization},
{NULL, NULL}
};
Expand Down
49 changes: 49 additions & 0 deletions Python/pylifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -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_interpolation.h" // _PyInterpolation_InitTypes()
#include "pycore_long.h" // _PyLong_InitTypes()
#include "pycore_object.h" // _PyDebug_PrintTotalRefs()
Expand Down Expand Up @@ -97,6 +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_interp_references(PyInterpreterState *interp);
static void finalize_subinterpreters(void);
static void call_ll_exitfuncs(_PyRuntimeState *runtime);

Expand Down Expand Up @@ -2022,6 +2024,9 @@ _Py_Finalize(_PyRuntimeState *runtime)
// Wrap up existing "threading"-module-created, non-daemon threads.
wait_for_thread_shutdown(tstate);

// Wait for the interpreter's reference count to reach zero
wait_for_interp_references(tstate->interp);

// Make any remaining pending calls.
_Py_FinishPendingCalls(tstate);

Expand Down Expand Up @@ -2438,6 +2443,9 @@ Py_EndInterpreter(PyThreadState *tstate)
// Wrap up existing "threading"-module-created, non-daemon threads.
wait_for_thread_shutdown(tstate);

// Wait for the interpreter's reference count to reach zero
wait_for_interp_references(tstate->interp);

// Make any remaining pending calls.
_Py_FinishPendingCalls(tstate);

Expand Down Expand Up @@ -3464,6 +3472,47 @@ wait_for_thread_shutdown(PyThreadState *tstate)
Py_DECREF(threading);
}

/* Wait for the interpreter's reference count to reach zero.
See PEP 788. */
static void
wait_for_interp_references(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.
PyMutex_Unlock(&finalizing->mutex);
return;
}
PyMutex_Unlock(&finalizing->mutex);

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)) {
// Event set
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. */
break;
}
}
}

int Py_AtExit(void (*func)(void))
{
struct _atexit_runtime_state *state = &_PyRuntime.atexit;
Expand Down
Loading
Loading
0