Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

gh-133465: Allow PyErr_CheckSignals to be called without holding the GIL. #133466

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 3 commits into
base: main
Choose a base branch
Loading
from

Conversation

zackw
Copy link

@zackw zackw commented May 5, 2025

Addresses #133465. See there, or the commit message for the first commit in this PR, for rationale.

There are two commits: the first actually implements the change, and the second demonstrates the motivation for it by pulling a lot of uses of Py_(BEGIN|END)_ALLOW_THREADS within Python's stdlib out of loops.

This has been tested (lightly - just the built in testsuite) both with and without --disable-gil; however, I did not test --enable-optimizations nor --enable-experimental-jit.


📚 Documentation preview 📚: https://cpython-previews--133466.org.readthedocs.build/

@python-cla-bot
Copy link

python-cla-bot bot commented May 5, 2025

All commit authors signed the Contributor License Agreement.

CLA signed

@bedevere-app
Copy link

bedevere-app bot commented May 5, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

Comment on lines +1 to +7
:c:func:`PyErr_CheckSignals` has been changed to acquire the global
interpreter lock (GIL) itself, only when necessary (i.e. when it has work to
do). This means that modules that perform lengthy computations with the GIL
released may now call :c:func:`PyErr_CheckSignals` during those computations
without re-acquiring the GIL first. (However, it must be *safe to* acquire
the GIL at each point where :c:func:`PyErr_CheckSignals` is called. Also,
keep in mind that it can run arbitrary Python code before returning to you.)
Copy link
Contributor

Choose a reason for hiding this comment

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

A NEWS entry should be more concise, users can refer to docs for in depth explanations.

Copy link
Author

@zackw zackw May 5, 2025

Choose a reason for hiding this comment

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

Is this better?

:c:func:`PyErr_CheckSignals` has been made safe to call without holding the GIL.
It will acquire the GIL itself when it needs it.

Copy link
Member

Choose a reason for hiding this comment

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

A NEWS entry should be more concise, users can refer to docs for in depth explanations.

@StanFromIreland, I disagree. AFAIK you aren't a core dev or triager, I wish you wouldn't give other contributors questionable advice without a reference in such an authoritive-sounding way.

Copy link
Member

Choose a reason for hiding this comment

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

I would update this and focus on how the new API differs from the old one: IIUC, "can be called without the GIL [or whatever PC phrasing] and doesn't acquire it unless a signal's handler needs to be called."

@zackw
Copy link
Author

zackw commented May 5, 2025

I am unable to reproduce the failure of test_queue, seen in https://github.com/python/cpython/actions/runs/14844414631/job/41674826787?pr=133466, even after running the test repeatedly with high levels of --parallel-threads. I will update the branch with a fix for the doc issue shortly.

zackw added 2 commits May 5, 2025 17:29
Compiled-code modules that implement time-consuming operations that
don’t require manipulating Python objects, are supposed to call
PyErr_CheckSignals frequently throughout each such operation, so that
if the user interrupts the operation with control-C, it is cancelled
promptly.  In the normal case where no signals are pending,
PyErr_CheckSignals is cheap; however, callers must hold the GIL,
and compiled-code modules that implement time-consuming operations
are also supposed to release the GIL during each such operation.
The overhead of reclaiming the GIL in order to call PyErr_CheckSignals,
and then releasing it again, sufficiently often for reasonable user
responsiveness, can be substantial.

If my understanding of the thread-state rules is correct,
PyErr_CheckSignals only *needs* the GIL if it has work to do.
*Checking* whether there is a pending signal, or a pending request
to run the cycle collector, requires only a couple of atomic loads.
Therefore:  Reorganize the logic of PyErr_CheckSignals and its close
relatives (_PyErr_CheckSignals and _PyErr_CheckSignalsTstate) so that
all the “do we have anything to do” checks are done in a batch before
anything that needs the GIL.  If any of them are true, acquire the GIL,
repeat the check (because another thread could have stolen the event
while we were waiting for the GIL), and then actually do the work,
enabling callers to *not* hold the GIL.

(There are some fine details here that I’d really appreciate a second
pair of eyes on — see the comments in the new functions
_PyErr_CheckSignalsHoldingGIL and _PyErr_CheckSignalsNoGIL.)
The source tree contains dozens of loops of this form:

    int res;
    do {
        Py_BEGIN_ALLOW_THREADS
        res = some_system_call(arguments...);
        Py_END_ALLOW_THREADS
    } while (res < 0 && errno == EINTR && !PyErr_CheckSignals());

Now that it is possible to call PyErr_CheckSignals without holding the
GIL, the locking operations can be moved out of the loop:

    Py_BEGIN_ALLOW_THREADS
    do {
        res = some_system_call(arguments...);
    } while (res < 0 && errno == EINTR && !PyErr_CheckSignals());
    Py_END_ALLOW_THREADS

This demonstrates the motivation for making it possible to call
PyErr_CheckSignals without holding the GIL.  It shouldn’t make any
measurable difference performance-wise for _these_ loops, which almost
never actually cycle; but for loops that do cycle many times it’s very
much desirable to not take and release the GIL every time through.

In some cases I also moved uses of _Py_(BEGIN|END)_SUPPRESS_IPH, which
is often paired with Py_(BEGIN|END)_ALLOW_THREADS, to keep the pairing
intact.  It was already considered safe to call PyErr_CheckSignals
from both inside and outside an IPH suppression region.

More could be done in this vein: I didn’t change any loops where the
inside of the loop was more complicated than a single system call,
_except_ that I did refactor py_getentropy and py_getrandom (in
bootstrap_hash.c) to make it possible to move the unlock and lock
outside the loop, demonstrating a more complicated case.
@zackw zackw force-pushed the check-signals-without-gil branch from 40097b8 to 3e25a93 Compare May 5, 2025 21:29
Copy link
Member

@ZeroIntensity ZeroIntensity left a comment

Choose a reason for hiding this comment

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

I haven't fully decided how I feel about this yet. I agree with the motivation, but PyGILState_Ensure is evil. We might be able to sidestep most of the issues here, though (for one, signal handling isn't done in subinterpreters, so we don't have to worry about interpreter-guessing issues).

My main concern is that we're changing something that's in the stable ABI. That's generally a big no-no, because those are supposed to have a "frozen" interface. We might want this in a new API (e.g., something like PyErr_CheckSignalsFast).

Comment on lines +1900 to +1901
/* FIXME: Given that we already have 'tstate', is there a more efficient
way to do this? */
Copy link
Member

Choose a reason for hiding this comment

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

Yes, you want PyThreadState_Swap(tstate).

Copy link
Author

Choose a reason for hiding this comment

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

The documentation for that function says "Swap the current thread state with the thread state given by the argument tstate, which may be NULL. The global interpreter lock must be held and is not released." So that really doesn't sound like what I should be using here.

What I think I need is more like PyEval_RestoreThread(tstate), except that (in GIL builds) if tstate already holds the GIL it should not deadlock attempting to acquire it again, and (in free-threaded builds) whatever the equivalent of that statement is. (I am only just now beginning to learn how free-threaded mode works.)

Copy link
Member

Choose a reason for hiding this comment

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

The 3.13 docs for PyThreadState_Swap are wrong!

Ignore the phrase "holds the GIL". Read it as "hold an attached thread state". You always need a thread state to call the C API, in both FT and GIL-icious builds.

Copy link
Member

Choose a reason for hiding this comment

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

@zackw Please fix this.

Determine whether there is actually any work needing to be done.
If so, acquire the GIL if necessary, and do that work. */
static int
_PyErr_CheckSignalsNoGIL(PyThreadState *tstate, bool cycle_collect)
Copy link
Member

Choose a reason for hiding this comment

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

Bikeshedding: let's call this "NoTstate", because you'll still need a thread state on free-threaded builds.

Copy link
Author

Choose a reason for hiding this comment

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

It's a little weird to name a function BlahNoTstate when its first argument is a tstate. How about _PyErr_CheckSignals_MaybeDetached for this one and _PyErr_CheckSignals_Attached for the one that does require an attached thread state to call?

(Also, given that these are static functions, possibly they should be named more like check_signals_maybe_detached and check_signals_attached? I do not fully grok the coding style in this file.)

Copy link
Member

Choose a reason for hiding this comment

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

Sure, whatever. Just don't call it "GIL" :)

/* If this thread does not have a thread state at all, then it has
never been associated with the Python runtime, so it should not
attempt to handle signals or run the cycle collector. */
if (!tstate) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure we should ignore this. This seems like blatant misuse--either return a failure or emit a fatal error.

Copy link
Author

Choose a reason for hiding this comment

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

All callers of this function get the tstate argument from PyGILState_GetThisThreadState(). I could be wrong about this, but I got the impression that that function could return NULL under circumstances where it would not be misuse to call PyErr_CheckSignals, such as the thread state merely having been set to NULL via PyThreadState_Swap. Perhaps it is the comment that is wrong? Anyway, the contract of PyErr_CheckSignals has always been that it fails if and only if a Python signal handler raised an exception, so it seemed safest to me to treat "we don't have a thread state" as "nothing to do" rather than some form of failure. I'm happy to be persuaded otherwise.

Copy link
Member

Choose a reason for hiding this comment

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

PyGILState_GetThisThreadState only returns NULL when the thread hasn't ever had a thread state. That hasn't ever been supported for PyErr_CheckSignals, which is why I'm worried about implicitly failing.

Copy link
Member

Choose a reason for hiding this comment

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

Looks all right to me in the new version of the PR -- the new function might be called under those circumstances, and no tstate means that it's definitely not the main thread of the main interpreter, so ignoring the call feels like the right thing to do. Right?

@ZeroIntensity
Copy link
Member

TSan failure looks unrelated. I've restarted the job for you.

@zackw
Copy link
Author

zackw commented May 6, 2025

@ZeroIntensity wrote:

I agree with the motivation, but PyGILState_Ensure is evil.

Yikes. I knew it could be trouble but I didn't know it could be that bad. I'm still reading the PEP and will have more to say once I have finished.

We might want this in a new API (e.g., something like PyErr_CheckSignalsFast).

I would argue that relaxing a requirement is not a breaking change, but I'm willing to give the function a new name if you all think it is necessary.

@zackw
Copy link
Author

zackw commented May 6, 2025

So I think PEP 788 is trying to solve a related problem with thread states to the one I have in this PR, but it doesn't directly address my problem.

I set out to make PyErr_CheckSignals be a function that could be called whether or not the calling thread has an attached thread state. Perhaps this would be clearer with an example: 3e25a93 (the second of the two commits in this PR) makes this abstract change...

     int res;
+    Py_BEGIN_ALLOW_THREADS
     do {
-        Py_BEGIN_ALLOW_THREADS
         res = some_system_call(arguments...);
-        Py_END_ALLOW_THREADS
     } while (res < 0 && errno == EINTR && !PyErr_CheckSignals());
+    Py_END_ALLOW_THREADS

to some but not all of the loops with that structure in the CPython codebase.

PyGILState_Ensure() is the only existing C-API function (as far as I can tell) that you're allowed to call from either inside or outside a Py_BEGIN_ALLOW_THREADS ... Py_END_ALLOW_THREADS block, and it promises that when it returns you'll have an attached thread state. PyEval_RestoreThread (either directly or in the form of Py_BLOCK_THREADS) deadlocks in GIL builds if called by a thread that already holds the GIL, so that's no good; the lower-level _PyThreadState_Attach appears to have the same problem; PyThreadState_Swap does something unrelated.

But PyGILState_Ensure does considerably more than what I need. If I understood PEP 788 correctly, it's the bits I don't need -- particularly, creation of a temporary thread state for a thread that has never had one -- that are troublesome. PEP 788 sets out to fix those bits, but it doesn't add a new API that does the specific thing I need ("reattach this thread's state if it's detached") without any of the extra bits.


Tangentially, part of the problem is that PyErr_CheckSignals doesn't take an interpreter or thread-state argument, so I do need PyGILState_Ensure's machinery for guessing which thread state to reattach; this is perhaps an argument for leaving it alone and adding a PyErr_CheckSignalsDetached(tstate) function as suggested above. Connected to that is that Py_BEGIN_ALLOW_THREADS doesn't overtly give you the thread state handle it just detached; it's a hidden variable. I'm tempted to suggest soft-deprecating BEGIN/END_ALLOW_THREADS in favor of a new macro which would be used like this (for the same sample loop as above):

    int res;
    Py_WITH_DETACHED_TSTATE (ts) {
        do {
            res = some_system_call(arguments...);
         } while (res < 0 && errno == EINTR
                  && !PyErr_CheckSignalsDetached(ts));
    }

(A macro like this can be implemented in plain C by off-label use of a for loop.)

I'm open to helping with work toward this end, but I would very much like to find a way to do it independently of this PR, which I would like to keep focused on the goal described in #133465.

@ZeroIntensity
Copy link
Member

I think we don't have to worry about the cross-interpreter problems because subinterpreters can't handle signals anyway--that's a job for the main thread and main interpreter. My approach would be to check PyGILState_GetThisThreadState() == &_PyRuntime._main_interpreter._initial_thread. That shields us from any concurrent finalization shenanigans, so it should be a bit more thread-safe. If that passes, PyGILState_Ensure should be safe.

@ZeroIntensity
Copy link
Member

Also cc @encukou @vstinner as members of the C API WG. I'm not sure what the rules are for extending functionality of stable APIs. Should this be in a new function?

@encukou
Copy link
Member

encukou commented May 7, 2025

The main issue with this kind of extension is that code tested with new versions of Python will fail in older ones.
It can be extended, but I'd prefer adding a new function instead.

@zackw
Copy link
Author

zackw commented May 19, 2025

So I think we have consensus that this PR should introduce a new function rather than change the semantics of an existing one. I propose to call the new function PyErr_CheckSignalsDetached and I'll go ahead and start working on that change.

I see some related design issues that need to be resolved, though:

  • Should the new function take a PyThreadState* argument?

    If it does, it's substantially easier to implement, but callers need to supply that argument, which can be difficult, both because it might need to be passed down through several layers of intermediate functions and because Py_BEGIN_ALLOW_THREADS does not officially give one access to a PyThreadState (the _state variable it declares is an undocumented implementation detail).

  • Should one be allowed to call the new function with or without an attached thread state, or only without?

    Again this is a tradeoff between implementation and usage convenience. Implementation would be easier if we required users of the new function to not have an attached thread state. As far as I can tell, the only function with the semantics of "give me an attached thread state if I don't already have one, do nothing if I do" is PyGILState_Ensure, which, per PEP 788, we'd rather not introduce new uses of. PyEval_RestoreThread is documented to deadlock if called by a thread that already has an attached thread state; PyThreadState_Swap is documented to require the outgoing thread state to be attached.

    However, it would be substantially easier for extension module authors to use the new API if they didn't have to worry about whether they did or didn't hold an attached thread state at each point where they want to introduce a signals check/cancellation point.

  • Should the new function run the cycle collector?

    PyErr_CheckSignals only actually checks for signals if it's running in the main interpreter on the main thread. However, regardless of which interpreter or thread it's running on, it may run the cycle collector (the "do we need to run the cycle collector soon?" flag is also checkable without the GIL/an attached thread state). This is not currently documented (this PR adds it to the documentation en passant).

    If the new function shouldn't try to run the cycle collector, it's easier to implement; in particular, the question of how we access the correct thread state becomes simpler. However, the rationale for PyErr_CheckSignals having the side effect of running the cycle collector is

    Opportunistically check if the GC is scheduled to run and run it if we have a request. This is done here because native code needs to call this API if is going to run for some time without executing Python code to ensure signals are handled. Checking for the GC here allows long running native code to clean cycles created using the C-API even if it doesn't run the evaluation loop

    which would seem to apply equally to the new PyErr_CheckSignalsDetached.

  • While we're in here, do we need is_tripped anymore?

    The actual C signal handler sets three semi-redundant flags: the per-signal &Handlers[sig_num].tripped, the global variable is_tripped, and the _PY_SIGNALS_PENDING_BIT bit in the eval breaker word for _PyRuntime.main_tstate. As far as I can tell, is_tripped is fully redundant to the eval breaker bit. Is there anything that needs it that isn't apparent to a recursive grep of the CPython source tree?

@zackw zackw marked this pull request as draft May 19, 2025 15:14
.. note::
Any code that executes for a long time without returning to the
Python interpreter should call :c:func:`PyErr_CheckSignals()`
at reasonable intervals (at least once a millisecond) so that
Copy link
Member

Choose a reason for hiding this comment

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

That is too frequent for my taste. Use interactions feel "delay-less" under 10 msec, and for ^C 100 even msec feels very quick, and 1sec would still be acceptable. After a few seconds I would hit ^C again.

Copy link
Author

Choose a reason for hiding this comment

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

This is briefly discussed in the talk. If you add enough PyErr_CheckSignals calls to your extension module that every long-running loop can be interrupted, in practice this makes you do the check way too often, once every few tens of micro seconds. This is fine with the hypothetical new PyErr_CheckSignals_Detached, but with the old version, where you have to reclaim the GIL, it's way too costly. So in the talk I recommended looking at the actual system clock (with clock_gettime) and only calling PyErr_CheckSignals if a millisecond or more had gone by. That's faster than required for human responsiveness, yes, but the remaining overhead is the overhead of looking at the clock and you can't reduce that by doing the actual calls even less often. So that's where "at least once a millisecond" came from.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe explain that better in the docs then.

@ZeroIntensity
Copy link
Member

Let's open an issue with the C API working group so they can bicker about bikeshedding and the API.

@zackw
Copy link
Author

zackw commented May 19, 2025

In person at the sprints, Guido suggested that this PR should be split up into three: one each for the core functional changes, the docs changes, and the "look what we can do now" changes to various stdlib modules. I'll make that happen, but not until Wednesday. Meantime, can we please focus discussion on the list of questions I posted? @ZeroIntensity I'd appreciate it if you could open that issue; I'm tired enough that I'm making foolish mistakes.

Copy link
Member

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

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

I have some nits, but generally I am happy with the new API.

@zackw, please split the PR up in three:

  • This PR, keep only the changes to signalmodule.c (and a few needed changes including NEWS). Also add brief docs for the new API, emphasizing that it should be called without holding the GIL. (You may or may not care about the whatsnew/3.15.rst file, other will eventually update it.)

  • A separate PR, attached to the same issue, showing the various ways that using the new API can make things better.

  • A third PR that revamps the docs, linked to the new issue you created about that.

  • A doc-only PR

Comment on lines +1 to +7
:c:func:`PyErr_CheckSignals` has been changed to acquire the global
interpreter lock (GIL) itself, only when necessary (i.e. when it has work to
do). This means that modules that perform lengthy computations with the GIL
released may now call :c:func:`PyErr_CheckSignals` during those computations
without re-acquiring the GIL first. (However, it must be *safe to* acquire
the GIL at each point where :c:func:`PyErr_CheckSignals` is called. Also,
keep in mind that it can run arbitrary Python code before returning to you.)
Copy link
Member

Choose a reason for hiding this comment

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

I would update this and focus on how the new API differs from the old one: IIUC, "can be called without the GIL [or whatever PC phrasing] and doesn't acquire it unless a signal's handler needs to be called."

#endif

/* It is necessary to repeat all of the checks of global flags
that were done in _PyErr_CheckSignalsNoGIL. At the time of
Copy link
Member

Choose a reason for hiding this comment

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

That function does not exist -- not sure what you meant.

Comment on lines +1776 to +1777
checks and when we acquired the GIL, some other thread may have
processed the events that were flagged. Since we now hold the
Copy link
Member

Choose a reason for hiding this comment

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

No other thread could have processed the events (only the main thread of the main interpreter can) but new events might have been added.

Suggested change
checks and when we acquired the GIL, some other thread may have
processed the events that were flagged. Since we now hold the
checks and when we acquired the GIL, events may have been
flagged or unflagged. Since we now hold the

that needs an attached thread state. When called, 'tstate' must
be the thread state for the current thread, and it must be attached. */
static int
check_signals_attached(PyThreadState *tstate, bool cycle_collect)
{
Copy link
Member

Choose a reason for hiding this comment

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

Possibly start with assert(tstate != NULL); ???

/* If this thread does not have a thread state at all, then it has
never been associated with the Python runtime, so it should not
attempt to handle signals or run the cycle collector. */
if (!tstate) {
Copy link
Member

Choose a reason for hiding this comment

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

Looks all right to me in the new version of the PR -- the new function might be called under those circumstances, and no tstate means that it's definitely not the main thread of the main interpreter, so ignoring the call feels like the right thing to do. Right?

Comment on lines +1928 to +1930
VERIFYME: I *think* every piece of this expression is safe to
execute without holding the GIL and is already sufficiently
atomic. */
Copy link
Member

Choose a reason for hiding this comment

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

I double checked and it looks safe to me, so you can drop this comment.

Comment on lines +1900 to +1901
/* FIXME: Given that we already have 'tstate', is there a more efficient
way to do this? */
Copy link
Member

Choose a reason for hiding this comment

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

@zackw Please fix this.

@gvanrossum
Copy link
Member

@zackw: Sorry we didn't get a chance to say goodbye at the sprint.

Let me try answering your questions, briefly.

* [ ]  Should the new function take a `PyThreadState*` argument?

I agree with your current choice to not ask for a thread state -- we want the function to be as convenient as possible, and to add minimum friction.

* [ ]  Should one be allowed to call the new function _with or without_ an attached thread state, or only _without_?

I think only without. I can imagine many patterns that make it easier for extensions to choose between PyErr_CheckSignals and PyErr_CheckSignals_Detached.

* [ ]  Should the new function run the cycle collector?

I think not. Calling the cycle collector is important when holding the GIL because we might be creating/updating/destroying Python objects. But when not holding the GIL we shouldn't be doing that, so the set of objects shouldn't change at all! Other threads that are creating lots of objects from C should be calling PyErr_CheckSignals(), so they'll run the GC (unlike signal handlers, it's not limited to the main thread).

* [ ]  While we're in here, do we need `is_tripped` anymore?
  The actual C signal handler sets _three_ semi-redundant flags: the per-signal `&Handlers[sig_num].tripped`, the global variable `is_tripped`, and the `_PY_SIGNALS_PENDING_BIT` bit in the eval breaker word for `_PyRuntime.main_tstate`.  As far as I can tell, `is_tripped` is fully redundant to the eval breaker bit.  Is there anything that needs it that isn't apparent to a recursive grep of the CPython source tree?

I don't know, but it sounds like a "here be dragons" area to me. Let's not try to fix this and then found out we shouldn't have. Someone else (or you :-) could endeavor to remove is_tripped at a later point.

@gvanrossum
Copy link
Member

It does look like you need to do a manual merge of a newer main branch before we merge this, since the branch is flagged as having conflicts. I recommend doing that at the latest possible moment (e.g. when you've received all the Approvals you need) so you won't have to do twice. Maybe moving the docs and other files to new PRs will remove the conflict.

VERIFYME: I *think* every piece of this expression is safe to
execute without holding the GIL and is already sufficiently
atomic. */
if ((!_Py_ThreadCanHandleSignals(tstate->interp)
Copy link
Member

Choose a reason for hiding this comment

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

This is missing a check for the remote debugger interface. Otherwise this will not run the remote debugger code when users call this function without the GIL

Copy link
Member

Choose a reason for hiding this comment

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

The checks are the same ones that _PyRunRemoteDebugger do before entering the actual code

@encukou
Copy link
Member

encukou commented May 20, 2025

Should the new function take a PyThreadState* argument? Py_BEGIN_ALLOW_THREADS does not officially give one access to a PyThreadState (the _state variable it declares is an undocumented implementation detail).

I'll dissent here. It should, just like Py_END_ALLOW_THREADS explicitly grabs the locally saved state.

A new macro like Py_CHECK_SIGNALS would both keep the secret variable hidden and make this easy to use.

(For the record, it's documented, the question being is whether that's a guarantee of how things work or just an illustration. In my opinion, it is at least an implementation guide for non-C/C++ users.)

@gvanrossum
Copy link
Member

Should the new function take a PyThreadState* argument? Py_BEGIN_ALLOW_THREADS does not officially give one access to a PyThreadState (the _state variable it declares is an undocumented implementation detail).

I'll dissent here. It should, just like Py_END_ALLOW_THREADS explicitly grabs the locally saved state.

A new macro like Py_CHECK_SIGNALS would both keep the secret variable hidden and make this easy to use.

(For the record, it's documented, the question being is whether that's a guarantee of how things work or just an illustration. In my opinion, it is at least an implementation guide for non-C/C++ users.)

I don't understand why those save/restore operations need to be done using a macro. They could just as easily be done by a function that does that save/restore behavior itself. I don't want to turn the old PyErr_CheckSignals (unchanged in functionality, just refactored) into a macro, and I don't believe things are so perf-critical that the first checks in PyErr_CheckSignals_Detached need to be a macro.

So I still don't think there's anything wrong with calling a function that retrieves and restores the tstate.

@encukou
Copy link
Member

encukou commented May 20, 2025

Well, explicit is better than implicit.

The code between Py_BEGIN_ALLOW_THREADS and Py_END_ALLOW_THREADS is usually low-level, but that's not a requirement. It can do anything. Including attaching thread states to do Python work, or even making (sub)interpreters come and go. How sure can we be that the this state can't be clobbered?

For example:

#include <Python.h>
#include <stdio.h>

#define N_ITERATIONS 2
static void call_into_some_library(void);

// For simplicity, store our main interpreter globally
PyInterpreterState *main_interp = NULL;

static PyObject *
demo_func(PyObject *mod, PyObject *arg) {
    main_interp = PyInterpreterState_Get();

    printf("Start! This thread state = %p\n",
           PyGILState_GetThisThreadState());

    Py_BEGIN_ALLOW_THREADS;
    for (int i=0; i<N_ITERATIONS; i++) {
        // Do some C work using some library...
        call_into_some_library();
        // if PyGILState_GetThisThreadState() is NULL, the
        // PyErr_CheckSignals from this PR will crash
        printf("%d/%d: this thread state = %p\n", i, N_ITERATIONS,
               PyGILState_GetThisThreadState());
    }
    Py_END_ALLOW_THREADS;
    Py_RETURN_NONE;
}

// Meanwhile, the library wants to do some Python...

static void
call_into_some_library(void) {
    /* ... */
    if (PyThreadState_GetUnchecked() == NULL) {
        // no thread state attached!
        // so make one & attach it
        PyThreadState *ts = PyThreadState_New(main_interp);
        PyEval_AcquireThread(ts);
        // call some fun Python API
        PyObject *s = PyUnicode_FromString("all OK down here\n");
        PyObject_Print(s, stdout, Py_PRINT_RAW);
        Py_DECREF(s);
        // and get rid of the thread state
        PyThreadState_Clear(ts);
        PyThreadState_DeleteCurrent();
        // by this we have reset "this" thread state to NULL
    }
    else { /* ... */ }
}

static PyModuleDef module = {
    .m_name = "extension",
    .m_methods = (PyMethodDef[]) {
        {"demo_func", demo_func, METH_NOARGS},
        {0},
    },
};

PyMODINIT_FUNC
PyInit_extension(void) {
    return PyModuleDef_Init(&module);
}

@encukou
Copy link
Member

encukou commented May 20, 2025

I don't understand why those save/restore operations need to be done using a macro. They could just as easily be done by a function that does that save/restore behavior itself.

I agree with that part, the macros could just have been functions. But they should be explicit about the state they want to get back to.

@vstinner
Copy link
Member

The new function is implemented with PyGILState_Ensure() and PyGILState_Release() which doesn't support sub-interpreters. I'm not sure if it's a good idea to add such new function. We may check the wrong interpreter. The current PyErr_CheckSignals() API doesn't have such issue.

    PyGILState_STATE st = PyGILState_Ensure();
    int err = check_signals_attached(tstate, cycle_collect);
    PyGILState_Release(st);

@ZeroIntensity
Copy link
Member

It might be fine, because subinterpreters don't currently support handling signals anyway. But, I don't know if that will always be the case. I was talking to Eric at the sprints about handling signals in any interpreter active in the main thread.

@vstinner
Copy link
Member

It might be fine, because subinterpreters don't currently support handling signals anyway.

Let's say that the function is called in a subinterpreter running in the main thread, and PyGILState_Ensure() picks instead the main thread of the main interpreter: we can handle signals in this case, whereas subinterpreters doesn't support handling signals. There is a problem, no?

@ZeroIntensity
Copy link
Member

Hmm, yeah, that doesn't sound great.

@gvanrossum
Copy link
Member

Yeah, I think we need a somewhat different design to handle subinterpreters and free threading smoothly.

And PyErr_CheckSignals may need to be changed as well? Maybe the forward-thinking approach would be a single function then??? (Sorry @zackw).

@ZeroIntensity
Copy link
Member

int PyErr_CheckSignals_Detached(PyInterpreterState *interp) is probably good enough. I don't think we'll need anything different for free-threading.

@vstinner
Copy link
Member

vstinner commented May 22, 2025

I'm not convinced that we have to provide a way to "Allow PyErr_CheckSignals to be called without holding the GIL". PEP 788 shows that it's quite complicated to acquire/release the GIL, there are corner cases and we need a new (more elaborated) API to prevent bugs/crashes.

int PyErr_CheckSignals_Detached(PyInterpreterState *interp)

Is there a risk that the interpreter pointer can become a dangling pointer?

@ZeroIntensity
Copy link
Member

Is there a risk that the interpreter pointer can become a dangling pointer?

Yes, but that's true of the existing APIs too. If PEP 788 is accepted, we can change this to use a strong interpreter reference.

@gvanrossum
Copy link
Member

The function already checks that it is the main thread. Is there a way to check that we’re in the main interpreter? Assuming we can check those without having the GIL or a tstate, would that simplify the requirements? Or is there still ambiguity?

@ZeroIntensity
Copy link
Member

I think the goal is to eventually get signal handling working in subinterpreters that are running in the main thread too.

Obligatory ping @ericsnowcurrently

@ericsnowcurrently
Copy link
Member

Yeah, that's a possibility. We'll have to see what makes the most sense.

@gvanrossum
Copy link
Member

I think the goal is to eventually get signal handling working in subinterpreters that are running in the main thread too.

Let's let the folks working on that feature worry about it.

But is there way to check that you're in the main interpreter?

@ZeroIntensity
Copy link
Member

_Py_IsMainInterpreter

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.