Skip to content

Commit c6f2373

Browse files
committed
gh-134075: Document how to make C extensions interruptible
1 parent d3ada45 commit c6f2373

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

‎Doc/c-api/exceptions.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,11 @@ Signal Handling
701701
702702
:c:func:`!PyErr_CheckSignals` should be called by long-running C code
703703
frequently enough so that the response appears immediate to humans.
704+
See :ref:`extending-interruptible` for guidance and an example.
705+
706+
The caller must have an :term:`attached thread state`.
707+
This function can execute arbitrary Python code, so objects visible to
708+
that code must be in a consistent state before it is called.
704709
705710
Handlers invoked by this function currently include:
706711

‎Doc/extending/extending.rst‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,100 @@ call to :c:func:`PyErr_SetString` as shown below::
187187
}
188188

189189

190+
.. _extending-interruptible:
191+
192+
Making long-running operations interruptible
193+
============================================
194+
195+
An extension that spends a long time in C code should call
196+
:c:func:`PyErr_CheckSignals` periodically so that users can interrupt it with
197+
:kbd:`Ctrl-C`. Python defers running signal handlers until it is safe to execute
198+
Python code. Without these checks, a signal handler may not run until the
199+
operation returns to the interpreter, even if the extension releases the
200+
:term:`global interpreter lock` (GIL). See :mod:`signal` for details.
201+
202+
When :c:func:`PyErr_CheckSignals` returns ``-1``, an exception is already set.
203+
Stop the operation, release any resources it owns, and propagate the exception
204+
to the caller, as described in :ref:`extending-errors`. Do not clear or replace
205+
the exception: although the default handler for :py:data:`signal.SIGINT` raises
206+
:exc:`KeyboardInterrupt`, a user-defined handler can raise any exception.
207+
A return value of ``0`` means that execution can continue, including when a
208+
handler ran without raising an exception.
209+
210+
Choosing safe places to check
211+
-----------------------------
212+
213+
:c:func:`PyErr_CheckSignals` can execute arbitrary Python code before returning.
214+
That code may call back into your extension or modify objects it uses. Before
215+
each check, put shared state into a consistent state and release locks that a
216+
callback might acquire. Keep strong references to objects needed after the
217+
check, and revalidate assumptions about mutable data before using it again.
218+
See :ref:`thinice` for related pitfalls with borrowed references.
219+
220+
The calling thread must have an :term:`attached thread state`. On builds with
221+
the GIL, this also means holding the GIL. When processing data with the thread
222+
state detached, divide the work into chunks and reattach it before each check.
223+
In particular, call :c:func:`PyErr_CheckSignals` after
224+
:c:macro:`Py_END_ALLOW_THREADS`, not between :c:macro:`Py_BEGIN_ALLOW_THREADS`
225+
and :c:macro:`Py_END_ALLOW_THREADS`. This requirement also applies to
226+
:term:`free-threaded <free threading>` builds; see
227+
:ref:`detaching-thread-state`.
228+
229+
For example, this function inverts the bits in a :class:`bytes` object and
230+
returns a new :class:`bytes` object. It checks for interruptions between chunks
231+
and discards the partial result if an exception is raised::
232+
233+
static PyObject *
234+
spam_invert(PyObject *self, PyObject *arg)
235+
{
236+
char *source;
237+
Py_ssize_t length;
238+
if (PyBytes_AsStringAndSize(arg, &source, &length) < 0) {
239+
return NULL;
240+
}
241+
PyObject *result = PyBytes_FromStringAndSize(NULL, length);
242+
if (result == NULL) {
243+
return NULL;
244+
}
245+
unsigned char *dest = (unsigned char *)PyBytes_AS_STRING(result);
246+
const Py_ssize_t chunk_size = 65536;
247+
Py_ssize_t offset = 0;
248+
while (offset < length) {
249+
Py_ssize_t count = Py_MIN(chunk_size, length - offset);
250+
Py_BEGIN_ALLOW_THREADS
251+
for (Py_ssize_t i = 0; i < count; i++) {
252+
dest[offset + i] = (unsigned char)source[offset + i] ^ 0xff;
253+
}
254+
Py_END_ALLOW_THREADS
255+
offset += count;
256+
if (PyErr_CheckSignals() < 0) {
257+
Py_DECREF(result);
258+
return NULL;
259+
}
260+
}
261+
return result;
262+
}
263+
264+
The input is immutable and the caller keeps it alive for the duration of the
265+
call. The output is private to this function until it returns, so callbacks
266+
cannot observe a partially filled result. No Python C API calls are made while
267+
the thread state is detached.
268+
269+
Choose a chunk size that balances the overhead of checking and reattaching the
270+
thread state against the time a user must wait for an interruption. The size
271+
above is illustrative; the appropriate interval depends on how much work each
272+
iteration performs. A check outside a long-running inner loop does not make
273+
that loop interruptible. Likewise, a single blocking call to an external
274+
library may need a timeout or a cancellation mechanism provided by that library
275+
so that control returns to a place where you can check for interruptions.
276+
277+
Python signal handlers run only in the main thread of the main interpreter.
278+
Calling :c:func:`PyErr_CheckSignals` in a worker thread does not run those
279+
handlers there. To cancel work in other threads, arrange for the main thread
280+
to handle the signal and communicate cancellation to the workers using a
281+
thread-safe mechanism. See :ref:`signals-and-threads`.
282+
283+
190284
.. _compilation:
191285

192286
Embedding an extension

0 commit comments

Comments
 (0)