Movatterモバイル変換


[0]ホーム

URL:


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

Sign up
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
zackw wants to merge3 commits intopython:main
base:main
Choose a base branch
Loading
fromMillionConcepts:check-signals-without-gil

Conversation

zackw
Copy link

@zackwzackw commentedMay 5, 2025
edited by github-actionsbot
Loading

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 ofPy_(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-botbot commentedMay 5, 2025
edited
Loading

All commit authors signed the Contributor License Agreement.

CLA signed

@bedevere-app
Copy link

Most changes to Pythonrequire a NEWS entry. Add one using theblurb_it web app or theblurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply theskip 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

@zackwzackwMay 5, 2025
edited
Loading

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

I am unable to reproduce the failure oftest_queue, seen inhttps://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 added2 commitsMay 5, 2025 17:29
Compiled-code modules that implement time-consuming operations thatdon’t require manipulating Python objects, are supposed to callPyErr_CheckSignals frequently throughout each such operation, so thatif the user interrupts the operation with control-C, it is cancelledpromptly.  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 operationsare 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 userresponsiveness, 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 requestto run the cycle collector, requires only a couple of atomic loads.Therefore:  Reorganize the logic of PyErr_CheckSignals and its closerelatives (_PyErr_CheckSignals and _PyErr_CheckSignalsTstate) so thatall the “do we have anything to do” checks are done in a batch beforeanything that needs the GIL.  If any of them are true, acquire the GIL,repeat the check (because another thread could have stolen the eventwhile 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 secondpair 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 theGIL, 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_THREADSThis demonstrates the motivation for making it possible to callPyErr_CheckSignals without holding the GIL.  It shouldn’t make anymeasurable difference performance-wise for _these_ loops, which almostnever actually cycle; but for loops that do cycle many times it’s verymuch desirable to not take and release the GIL every time through.In some cases I also moved uses of _Py_(BEGIN|END)_SUPPRESS_IPH, whichis often paired with Py_(BEGIN|END)_ALLOW_THREADS, to keep the pairingintact.  It was already considered safe to call PyErr_CheckSignalsfrom both inside and outside an IPH suppression region.More could be done in this vein: I didn’t change any loops where theinside of the loop was more complicated than a single system call,_except_ that I did refactor py_getentropy and py_getrandom (inbootstrap_hash.c) to make it possible to move the unlock and lockoutside the loop, demonstrating a more complicated case.
@zackwzackwforce-pushed thecheck-signals-without-gil branch from40097b8 to3e25a93CompareMay 5, 2025 21:29
Copy link
Member

@ZeroIntensityZeroIntensity 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, butPyGILState_Ensure isevil. 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 likePyErr_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 wantPyThreadState_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 likePyEval_RestoreThread(tstate),except that (in GIL builds) iftstate 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 forPyThreadState_Swap are wrong!

Ignore the phrase "holds the GIL". Read it as "hold anattached 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 functionBlahNoTstate 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 likecheck_signals_maybe_detached andcheck_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" :)

gvanrossum reacted with thumbs up emoji
/* 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 fromPyGILState_GetThisThreadState(). I could be wrong about this, but I got the impression that that function could return NULL under circumstances where it wouldnot be misuse to callPyErr_CheckSignals, such as the thread state merely having been set to NULL viaPyThreadState_Swap. Perhaps it is the comment that is wrong? Anyway, the contract ofPyErr_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 returnsNULL when the thread hasn't ever had a thread state. That hasn't ever been supported forPyErr_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

@ZeroIntensity wrote:

I agree with the motivation, butPyGILState_Ensure isevil.

Yikes. I knew it could be trouble but I didn't know it could bethat 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 likePyErr_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 commentedMay 6, 2025
edited
Loading

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

I set out to makePyErr_CheckSignals be a function that could be calledwhether 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

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

PyGILState_Ensure() is the onlyexisting C-API function (as far as I can tell) that you're allowed to call fromeither inside or outside aPy_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 ofPy_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.

ButPyGILState_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 itdoesn't add a new API that does the specific thing I need ("reattach this thread's stateif it's detached") without any of the extra bits.


Tangentially, part of the problem is thatPyErr_CheckSignals doesn't take an interpreter or thread-state argument, so Ido needPyGILState_Ensure's machinery for guessing which thread state to reattach; this is perhaps an argument for leaving it alone and adding aPyErr_CheckSignalsDetached(tstate) function as suggested above. Connected to that is thatPy_BEGIN_ALLOW_THREADS doesn'tovertly 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):

intres;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 afor loop.)

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

@ZeroIntensity
Copy link
Member

Ithink 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 checkPyGILState_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

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

@zackw
Copy link
Author

zackw commentedMay 19, 2025
edited
Loading

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 functionPyErr_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 aPyThreadState* 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 becausePy_BEGIN_ALLOW_THREADS does not officially give one access to aPyThreadState (the_state variable it declares is an undocumented implementation detail).

  • Should one be allowed to call the new functionwith or without an attached thread state, or onlywithout?

    Again this is a tradeoff between implementation and usage convenience. Implementation would be easier if we required users of the new function tonot have an attached thread state. As far as I can tell, theonly function with the semantics of "give me an attached thread stateif I don't already have one, do nothing if I do" isPyGILState_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 touse 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 documentationen 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 thecorrect thread state becomes simpler. However, the rationale forPyErr_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 newPyErr_CheckSignalsDetached.

  • While we're in here, do we needis_tripped anymore?

    The actual C signal handler setsthree semi-redundant flags: the per-signal&Handlers[sig_num].tripped, the global variableis_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?

@zackwzackw marked this pull request as draftMay 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.

gpshead reacted with thumbs up emoji
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 enoughPyErr_CheckSignals calls to your extension module that every long-running loopcan be interrupted, in practice this makes you do the check way toooften, once every few tens ofmicro seconds. This is fine with the hypothetical newPyErr_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 (withclock_gettime) and only callingPyErr_CheckSignals if a millisecond or more had gone by. That's faster than required for human responsiveness, yes, but the remaining overhead is the overheadof 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 theC API working group so they can bicker about bikeshedding and the API.

gpshead reacted with thumbs up emoji

@zackw
Copy link
Author

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

@gvanrossumgvanrossum 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 otherthread could have processed the events (only the main thread of the main interpreter can) but new events might have been added.

Suggested change
checksandwhenweacquiredtheGIL,someotherthreadmayhave
processedtheeventsthatwereflagged.Sincewenowholdthe
checksandwhenweacquiredtheGIL,eventsmayhavebeen
flaggedorunflagged.Sincewenowholdthe

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 withassert(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 betweenPyErr_CheckSignalsandPyErr_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 callingPyErr_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 removeis_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

gvanrossum reacted with thumbs up emoji
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

Should the new function take aPyThreadState* 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 likePy_END_ALLOW_THREADS explicitly grabs the locally saved state.

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

(For the record, it'sdocumented, 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 aPyThreadState* 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 likePy_END_ALLOW_THREADS explicitly grabs the locally saved state.

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

(For the record, it'sdocumented, 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

Well, explicit is better than implicit.

The code betweenPy_BEGIN_ALLOW_THREADS andPy_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 thethis state can't be clobbered?

For example:

#include<Python.h>#include<stdio.h>#defineN_ITERATIONS 2staticvoidcall_into_some_library(void);// For simplicity, store our main interpreter globallyPyInterpreterState*main_interp=NULL;staticPyObject*demo_func(PyObject*mod,PyObject*arg) {main_interp=PyInterpreterState_Get();printf("Start! This thread state = %p\n",PyGILState_GetThisThreadState());Py_BEGIN_ALLOW_THREADS;for (inti=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 crashprintf("%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...staticvoidcall_into_some_library(void) {/* ... */if (PyThreadState_GetUnchecked()==NULL) {// no thread state attached!// so make one & attach itPyThreadState*ts=PyThreadState_New(main_interp);PyEval_AcquireThread(ts);// call some fun Python APIPyObject*s=PyUnicode_FromString("all OK down here\n");PyObject_Print(s,stdout,Py_PRINT_RAW);Py_DECREF(s);// and get rid of the thread statePyThreadState_Clear(ts);PyThreadState_DeleteCurrent();// by this we have reset "this" thread state to NULL    }else {/* ... */ }}staticPyModuleDefmodule= {    .m_name="extension",    .m_methods= (PyMethodDef[]) {        {"demo_func",demo_func,METH_NOARGS},        {0},    },};PyMODINIT_FUNCPyInit_extension(void) {returnPyModuleDef_Init(&module);}

@encukou
Copy link
Member

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 withPyGILState_Ensure() andPyGILState_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 currentPyErr_CheckSignals() API doesn't have such issue.

PyGILState_STATEst=PyGILState_Ensure();interr=check_signals_attached(tstate,cycle_collect);PyGILState_Release(st);

@ZeroIntensity
Copy link
Member

Itmight 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 asubinterpreter running in the main thread, andPyGILState_Ensure() picks instead the main thread of themain 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 commentedMay 22, 2025
edited
Loading

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.

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

@ZeroIntensity
Copy link
Member

_Py_IsMainInterpreter

gvanrossum reacted with thumbs up emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@gvanrossumgvanrossumgvanrossum left review comments

@pablogsalpablogsalpablogsal left review comments

@ZeroIntensityZeroIntensityZeroIntensity left review comments

@StanFromIrelandStanFromIrelandStanFromIreland left review comments

@gpsheadgpsheadAwaiting requested review from gpsheadgpshead is a code owner

@lysnikolaoulysnikolaouAwaiting requested review from lysnikolaoulysnikolaou is a code owner

Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

8 participants
@zackw@ZeroIntensity@encukou@gvanrossum@vstinner@ericsnowcurrently@pablogsal@StanFromIreland

[8]ページ先頭

©2009-2025 Movatter.jp