Skip to content

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 43 additions & 19 deletions Doc/c-api/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,8 @@ Querying the error indicator
Signal Handling
===============

See the :mod:`signal` module for an overview of signals and signal
handling.

.. c:function:: int PyErr_CheckSignals()

Expand All @@ -639,29 +641,51 @@ Signal Handling
single: SIGINT (C macro)
single: KeyboardInterrupt (built-in exception)

This function interacts with Python's signal handling.
This function is to be called by long-running C code that wants to
be interruptible by user requests, such as by pressing Ctrl-C.

When it is called from the main thread and under the main Python
interpreter, it checks whether any signals have been delivered to
the process, and if so, invokes the corresponding Python-level
signal handler (if there is one) for each signal.

:c:func:`PyErr_CheckSignals()` attempts to call signal handlers
for each signal that has been delivered since the last time it
was called. If all signal handlers complete successfully, it
returns ``0``. However, if a signal handler raises an exception,
that exception is stored in the error indicator for the main thread,
and :c:func:`PyErr_CheckSignals()` immediately returns ``-1``.
(When this happens, some of the pending signals may not have had
their signal handlers called; they will be called the next time
:c:func:`PyErr_CheckSignals()` is called.)

Callers of :c:func:`PyErr_CheckSignals()` should treat a ``-1``
return value the same as any other failure of a C-API function;
they should immediately cease work, clean up (deallocating
resources, etc.) and propagate the failure status to their
callers.

When this function is called from other than the main thread, or
other than the main Python interpreter, it does not invoke any
signal handlers, and it always returns ``0``.

Regardless of context, calling this function may have the side
effect of running the cyclic garbage collector.

If the function is called from the main thread and under the main Python
interpreter, it checks whether a signal has been sent to the processes
and if so, invokes the corresponding signal handler. If the :mod:`signal`
module is supported, this can invoke a signal handler written in Python.

The function attempts to handle all pending signals, and then returns ``0``.
However, if a Python signal handler raises an exception, the error
indicator is set and the function returns ``-1`` immediately (such that
other pending signals may not have been handled yet: they will be on the
next :c:func:`PyErr_CheckSignals()` invocation).

If the function is called from a non-main thread, or under a non-main
Python interpreter, it does nothing and returns ``0``.

This function can be called by long-running C code that wants to
be interruptible by user requests (such as by pressing Ctrl-C).
.. warning::
This function may execute arbitrary Python code before returning
to its caller.

.. note::
The default Python signal handler for :c:macro:`!SIGINT` raises the
:exc:`KeyboardInterrupt` exception.
This function can be called without an :term:`attached thread state`
(see :ref:`threads`). However, this function may internally
attach (and then release) a thread state (only if it has any
work to do); it must be safe to do that at each point where
this function is called.

.. note::
The default Python signal handler for :c:macro:`!SIGINT` raises
the :exc:`KeyboardInterrupt` exception.

.. c:function:: void PyErr_SetInterrupt()

Expand Down
5 changes: 5 additions & 0 deletions Doc/c-api/init.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,11 @@ to be released to attach their thread state, allowing true multi-core parallelis
For example, the standard :mod:`zlib` and :mod:`hashlib` modules detach the
:term:`thread state <attached thread state>` when compressing or hashing data.

.. 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
it can be interrupted by the user.

.. _gilstate:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,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.)
Comment on lines +1 to +7
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.

4 changes: 2 additions & 2 deletions Modules/_io/fileio.c
Original file line number Diff line number Diff line change
Expand Up @@ -403,16 +403,16 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode,

errno = 0;
if (opener == Py_None) {
Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
#ifdef MS_WINDOWS
self->fd = _wopen(widename, flags, 0666);
#else
self->fd = open(name, flags, 0666);
#endif
Py_END_ALLOW_THREADS
} while (self->fd < 0 && errno == EINTR &&
!(async_err = PyErr_CheckSignals()));
Py_END_ALLOW_THREADS

if (async_err)
goto error;
Expand Down
2 changes: 0 additions & 2 deletions Modules/_io/winconsoleio.c
Original file line number Diff line number Diff line change
Expand Up @@ -647,9 +647,7 @@ read_console_w(HANDLE handle, DWORD maxlen, DWORD *readlen) {
if (WaitForSingleObjectEx(hInterruptEvent, 100, FALSE)
== WAIT_OBJECT_0) {
ResetEvent(hInterruptEvent);
Py_BLOCK_THREADS
sig = PyErr_CheckSignals();
Py_UNBLOCK_THREADS
if (sig < 0)
break;
}
Expand Down
8 changes: 4 additions & 4 deletions Modules/_multiprocessing/posixshmem.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ _posixshmem_shm_open_impl(PyObject *module, PyObject *path, int flags,
PyErr_SetString(PyExc_ValueError, "embedded null character");
return -1;
}
Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
fd = shm_open(name, flags, mode);
Py_END_ALLOW_THREADS
} while (fd < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
Py_END_ALLOW_THREADS

if (fd < 0) {
if (!async_err)
Expand Down Expand Up @@ -102,11 +102,11 @@ _posixshmem_shm_unlink_impl(PyObject *module, PyObject *path)
PyErr_SetString(PyExc_ValueError, "embedded null character");
return NULL;
}
Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
rv = shm_unlink(name);
Py_END_ALLOW_THREADS
} while (rv < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
Py_END_ALLOW_THREADS

if (rv < 0) {
if (!async_err)
Expand Down
4 changes: 2 additions & 2 deletions Modules/_multiprocessing/semaphore.c
Original file line number Diff line number Diff line change
Expand Up @@ -356,19 +356,19 @@ _multiprocessing_SemLock_acquire_impl(SemLockObject *self, int blocking,

if (res < 0 && errno == EAGAIN && blocking) {
/* Couldn't acquire immediately, need to block */
Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
if (!use_deadline) {
res = sem_wait(self->handle);
}
else {
res = sem_timedwait(self->handle, &deadline);
}
Py_END_ALLOW_THREADS
err = errno;
if (res == MP_EXCEPTION_HAS_BEEN_SET)
break;
} while (res < 0 && errno == EINTR && !PyErr_CheckSignals());
Py_END_ALLOW_THREADS
}

if (res < 0) {
Expand Down
32 changes: 16 additions & 16 deletions Modules/fcntlmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ fcntl_fcntl_impl(PyObject *module, int fd, int code, PyObject *arg)
}
}

Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
ret = fcntl(fd, code, (int)int_arg);
Py_END_ALLOW_THREADS
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
Py_END_ALLOW_THREADS
if (ret < 0) {
return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
}
Expand All @@ -103,11 +103,11 @@ fcntl_fcntl_impl(PyObject *module, int fd, int code, PyObject *arg)
memcpy(buf + len, guard, GUARDSZ);
PyBuffer_Release(&view);

Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
ret = fcntl(fd, code, buf);
Py_END_ALLOW_THREADS
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
Py_END_ALLOW_THREADS
if (ret < 0) {
return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
}
Expand Down Expand Up @@ -195,11 +195,11 @@ fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
}
}

Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
ret = ioctl(fd, code, int_arg);
Py_END_ALLOW_THREADS
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
Py_END_ALLOW_THREADS
if (ret < 0) {
return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
}
Expand All @@ -219,11 +219,11 @@ fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
memcpy(buf + len, guard, GUARDSZ);
ptr = buf;
}
Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
ret = ioctl(fd, code, ptr);
Py_END_ALLOW_THREADS
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
Py_END_ALLOW_THREADS
if (ret < 0) {
if (!async_err) {
PyErr_SetFromErrno(PyExc_OSError);
Expand Down Expand Up @@ -261,11 +261,11 @@ fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
memcpy(buf + len, guard, GUARDSZ);
PyBuffer_Release(&view);

Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
ret = ioctl(fd, code, buf);
Py_END_ALLOW_THREADS
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
Py_END_ALLOW_THREADS
if (ret < 0) {
return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
}
Expand Down Expand Up @@ -308,11 +308,11 @@ fcntl_flock_impl(PyObject *module, int fd, int code)
}

#ifdef HAVE_FLOCK
Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
ret = flock(fd, code);
Py_END_ALLOW_THREADS
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
Py_END_ALLOW_THREADS
#else

#ifndef LOCK_SH
Expand All @@ -335,11 +335,11 @@ fcntl_flock_impl(PyObject *module, int fd, int code)
return NULL;
}
l.l_whence = l.l_start = l.l_len = 0;
Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
ret = fcntl(fd, (code & LOCK_NB) ? F_SETLK : F_SETLKW, &l);
Py_END_ALLOW_THREADS
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
Py_END_ALLOW_THREADS
}
#endif /* HAVE_FLOCK */
if (ret < 0) {
Expand Down Expand Up @@ -439,11 +439,11 @@ fcntl_lockf_impl(PyObject *module, int fd, int code, PyObject *lenobj,
return NULL;
}
l.l_whence = whence;
Py_BEGIN_ALLOW_THREADS
do {
Py_BEGIN_ALLOW_THREADS
ret = fcntl(fd, (code & LOCK_NB) ? F_SETLK : F_SETLKW, &l);
Py_END_ALLOW_THREADS
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
Py_END_ALLOW_THREADS
}
if (ret < 0) {
return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
Expand Down
Loading
Loading