Skip to content

Commit 6c82e34

Browse files
committed
pythongh-133465: Efficient signal checks with detached thread state.
Add new C-API functions `PyErr_CheckSignalsDetached` and `PyErr_AreSignalsPending`. `PyErr_CheckSignalsDetached` can *only* be called by threads that *don’t* have an attached thread state. It does the same thing as `PyErr_CheckSignals`, except that it guarantees it will only reattach the supplied thread state if necessary in order to run signal handlers. (Also, it never runs the cycle collector.) `PyErr_AreSignalsPending` can be called with or without an attached thread state. It reports to its caller whether signals are pending, but never runs any handlers itself. Rationale: 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 canceled promptly. In the normal case where no signals are pending, PyErr_CheckSignals is cheap (two atomic memory operations); however, callers must have an attached thread state, and compiled-code modules that implement time-consuming operations are also supposed to detach their thread state during each such operation. The overhead of re-attaching a thread state in order to call PyErr_CheckSignals, and then releasing it again, sufficiently often for reasonable user responsiveness, can be substantial, particularly in traditional (non-free-threaded) builds. These new functions permit compiled-code modules to avoid that extra overhead.
1 parent 0f866cb commit 6c82e34

File tree

4 files changed

+108
-16
lines changed

4 files changed

+108
-16
lines changed

Doc/c-api/exceptions.rst

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ Signal Handling
642642
This function interacts with Python's signal handling.
643643
644644
If the function is called from the main thread and under the main Python
645-
interpreter, it checks whether a signal has been sent to the processes
645+
interpreter, it checks whether a signal has been sent to the process
646646
and if so, invokes the corresponding signal handler. If the :mod:`signal`
647647
module is supported, this can invoke a signal handler written in Python.
648648
@@ -653,7 +653,11 @@ Signal Handling
653653
next :c:func:`PyErr_CheckSignals()` invocation).
654654
655655
If the function is called from a non-main thread, or under a non-main
656-
Python interpreter, it does nothing and returns ``0``.
656+
Python interpreter, it does not check for pending signals, and always
657+
returns ``0``.
658+
659+
Regardless of calling context, this function may, as a side effect,
660+
run the cyclic garbage collector (see :ref:`supporting-cycle-detection`).
657661
658662
This function can be called by long-running C code that wants to
659663
be interruptible by user requests (such as by pressing Ctrl-C).
@@ -662,6 +666,47 @@ Signal Handling
662666
The default Python signal handler for :c:macro:`!SIGINT` raises the
663667
:exc:`KeyboardInterrupt` exception.
664668
669+
.. c:function:: int PyErr_CheckSignalsDetached(PyThreadState *tstate)
670+
671+
.. index::
672+
pair: module; signal
673+
single: SIGINT (C macro)
674+
single: KeyboardInterrupt (built-in exception)
675+
676+
This function is similar to :c:func:`PyErr_CheckSignals`. However, unlike
677+
that function, it must be called **without** an :term:`attached thread state`.
678+
The ``tstate`` argument must be the thread state that was formerly attached to
679+
the calling context (this is the value returned by :c:func:`PyEval_SaveThread`)
680+
and it must be safe to re-attach the thread state briefly.
681+
682+
If the ``tstate`` argument refers to the main thread and the main Python
683+
interpreter, this function checks whether any signals have been sent to the
684+
process, and if so, invokes the corresponding signal handlers. Otherwise it
685+
does nothing. If signal handlers do need to be run, the supplied thread state
686+
will be attached while they are run, then detached again afterward.
687+
688+
The return value is the same as for :c:func:`PyErr_CheckSignals`,
689+
i.e. ``-1`` if a signal handler raised an exception, ``0`` otherwise.
690+
691+
Unlike :c:func:`PyErr_CheckSignals`, this function never runs the cyclic
692+
garbage collector.
693+
694+
This function can be called by long-running C code that wants to
695+
be interruptible by user requests from within regions where it has
696+
detached the thread state, while minimizing the overhead of the check
697+
in the normal case of no pending signals.
698+
699+
.. c:function:: int PyErr_AreSignalsPending(PyThreadState *tstate)
700+
701+
This function returns a nonzero value if the execution context ``tstate``
702+
needs to process signals soon: that is, ``tstate`` refers to the main thread
703+
and the main Python interpreter, and signals have been sent to the process,
704+
and their handlers have not yet been run. Otherwise, it returns zero. It has
705+
no side effects.
706+
707+
.. note::
708+
This function may be called either with or without
709+
an :term:`attached thread state`.
665710
666711
.. c:function:: void PyErr_SetInterrupt()
667712

Include/pyerrors.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ PyAPI_FUNC(void) PyErr_WriteUnraisable(PyObject *);
235235

236236
/* In signalmodule.c */
237237
PyAPI_FUNC(int) PyErr_CheckSignals(void);
238+
PyAPI_FUNC(int) PyErr_CheckSignalsDetached(PyThreadState *state);
239+
PyAPI_FUNC(int) PyErr_AreSignalsPending(PyThreadState *state);
238240
PyAPI_FUNC(void) PyErr_SetInterrupt(void);
239241
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030A0000
240242
PyAPI_FUNC(int) PyErr_SetInterruptEx(int signum);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
New functions :c:func:`PyErr_CheckSignalsDetached` and
2+
:c:func:`PyErr_AreSignalsPending` for responding to signals
3+
from within C extension modules that detach the thread state.
4+
Patch by Zack Weinberg.

Modules/signalmodule.c

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,6 +1759,19 @@ _PySignal_Fini(void)
17591759
Py_CLEAR(state->ignore_handler);
17601760
}
17611761

1762+
/* used by PyErr_CheckSignalsDetached and _PyErr_CheckSignalsTstate */
1763+
static int process_signals(PyThreadState *tstate);
1764+
1765+
/* Declared in pyerrors.h */
1766+
int
1767+
PyErr_AreSignalsPending(PyThreadState *tstate)
1768+
{
1769+
if (!_Py_ThreadCanHandleSignals(tstate->interp)) {
1770+
return 0;
1771+
}
1772+
_Py_CHECK_EMSCRIPTEN_SIGNALS();
1773+
return _Py_atomic_load_int(&is_tripped);
1774+
}
17621775

17631776
/* Declared in pyerrors.h */
17641777
int
@@ -1781,23 +1794,60 @@ PyErr_CheckSignals(void)
17811794
_PyRunRemoteDebugger(tstate);
17821795
#endif
17831796

1784-
if (!_Py_ThreadCanHandleSignals(tstate->interp)) {
1785-
return 0;
1797+
return _PyErr_CheckSignalsTstate(tstate);
1798+
}
1799+
1800+
/* Declared in pyerrors.h */
1801+
int
1802+
PyErr_CheckSignalsDetached(PyThreadState *tstate)
1803+
{
1804+
int status = 0;
1805+
1806+
/* Unlike PyErr_CheckSignals, we do not check whether the GC is
1807+
scheduled to run. This function can only be called from
1808+
contexts without an attached thread state, and contexts that
1809+
don't have an attached thread state cannot generate garbage. */
1810+
1811+
#if defined(Py_REMOTE_DEBUG) && defined(Py_SUPPORTS_REMOTE_DEBUG)
1812+
_PyRunRemoteDebugger(tstate);
1813+
#endif
1814+
1815+
if (PyErr_AreSignalsPending(tstate)) {
1816+
PyEval_AcquireThread(tstate);
1817+
/* It is necessary to re-check whether any signals are pending
1818+
after re-attaching the thread state, because, while we were
1819+
waiting to acquire an attached thread state, the situation
1820+
might have changed. */
1821+
if (PyErr_AreSignalsPending(tstate)) {
1822+
status = process_signals(tstate);
1823+
}
1824+
PyEval_ReleaseThread(tstate);
17861825
}
1826+
return status;
1827+
}
17871828

1829+
/* Declared in cpython/pyerrors.h */
1830+
int
1831+
_PyErr_CheckSignals(void)
1832+
{
1833+
PyThreadState *tstate = _PyThreadState_GET();
17881834
return _PyErr_CheckSignalsTstate(tstate);
17891835
}
17901836

1791-
17921837
/* Declared in cpython/pyerrors.h */
17931838
int
17941839
_PyErr_CheckSignalsTstate(PyThreadState *tstate)
17951840
{
1796-
_Py_CHECK_EMSCRIPTEN_SIGNALS();
1797-
if (!_Py_atomic_load_int(&is_tripped)) {
1841+
if (!PyErr_AreSignalsPending(tstate)) {
17981842
return 0;
17991843
}
18001844

1845+
return process_signals(tstate);
1846+
}
1847+
1848+
static int
1849+
process_signals(PyThreadState *tstate)
1850+
{
18011851
/*
18021852
* The is_tripped variable is meant to speed up the calls to
18031853
* PyErr_CheckSignals (both directly or via pending calls) when no
@@ -1878,15 +1928,6 @@ _PyErr_CheckSignalsTstate(PyThreadState *tstate)
18781928
}
18791929

18801930

1881-
1882-
int
1883-
_PyErr_CheckSignals(void)
1884-
{
1885-
PyThreadState *tstate = _PyThreadState_GET();
1886-
return _PyErr_CheckSignalsTstate(tstate);
1887-
}
1888-
1889-
18901931
/* Simulate the effect of a signal arriving. The next time PyErr_CheckSignals
18911932
is called, the corresponding Python signal handler will be raised.
18921933

0 commit comments

Comments
 (0)