mirror of https://github.com/python/cpython
[3.13] gh-119241: Add HOWTO for free-threaded C API extensions (GH-119877) (#120693)
Some sections adapted from https://github.com/Quansight-Labs/free-threaded-compatibility/
written by Nathan Goldbaum.
(cherry picked from commit 02b272b702
)
Co-authored-by: Sam Gross <colesbury@gmail.com>
Co-authored-by: Nathan Goldbaum <nathan.goldbaum@gmail.com>
This commit is contained in:
parent
36b0052cb2
commit
8c129d99ef
|
@ -0,0 +1,254 @@
|
|||
.. highlight:: c
|
||||
|
||||
.. _freethreading-extensions-howto:
|
||||
|
||||
******************************************
|
||||
C API Extension Support for Free Threading
|
||||
******************************************
|
||||
|
||||
Starting with the 3.13 release, CPython has experimental support for running
|
||||
with the :term:`global interpreter lock` (GIL) disabled in a configuration
|
||||
called :term:`free threading`. This document describes how to adapt C API
|
||||
extensions to support free threading.
|
||||
|
||||
|
||||
Identifying the Free-Threaded Build in C
|
||||
========================================
|
||||
|
||||
The CPython C API exposes the ``Py_GIL_DISABLED`` macro: in the free-threaded
|
||||
build it's defined to ``1``, and in the regular build it's not defined.
|
||||
You can use it to enable code that only runs under the free-threaded build::
|
||||
|
||||
#ifdef Py_GIL_DISABLED
|
||||
/* code that only runs in the free-threaded build */
|
||||
#endif
|
||||
|
||||
Module Initialization
|
||||
=====================
|
||||
|
||||
Extension modules need to explicitly indicate that they support running with
|
||||
the GIL disabled; otherwise importing the extension will raise a warning and
|
||||
enable the GIL at runtime.
|
||||
|
||||
There are two ways to indicate that an extension module supports running with
|
||||
the GIL disabled depending on whether the extension uses multi-phase or
|
||||
single-phase initialization.
|
||||
|
||||
Multi-Phase Initialization
|
||||
..........................
|
||||
|
||||
Extensions that use multi-phase initialization (i.e.,
|
||||
:c:func:`PyModuleDef_Init`) should add a :c:data:`Py_mod_gil` slot in the
|
||||
module definition. If your extension supports older versions of CPython,
|
||||
you should guard the slot with a :c:data:`PY_VERSION_HEX` check.
|
||||
|
||||
::
|
||||
|
||||
static struct PyModuleDef_Slot module_slots[] = {
|
||||
...
|
||||
#if PY_VERSION_HEX >= 0x030D0000
|
||||
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
|
||||
#endif
|
||||
{0, NULL}
|
||||
};
|
||||
|
||||
static struct PyModuleDef moduledef = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
.m_slots = module_slots,
|
||||
...
|
||||
};
|
||||
|
||||
|
||||
Single-Phase Initialization
|
||||
...........................
|
||||
|
||||
Extensions that use single-phase initialization (i.e.,
|
||||
:c:func:`PyModule_Create`) should call :c:func:`PyUnstable_Module_SetGIL` to
|
||||
indicate that they support running with the GIL disabled. The function is
|
||||
only defined in the free-threaded build, so you should guard the call with
|
||||
``#ifdef Py_GIL_DISABLED`` to avoid compilation errors in the regular build.
|
||||
|
||||
::
|
||||
|
||||
static struct PyModuleDef moduledef = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
...
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC
|
||||
PyInit_mymodule(void)
|
||||
{
|
||||
PyObject *m = PyModule_Create(&moduledef);
|
||||
if (m == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
#ifdef Py_GIL_DISABLED
|
||||
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
|
||||
#endif
|
||||
return m;
|
||||
}
|
||||
|
||||
|
||||
General API Guidelines
|
||||
======================
|
||||
|
||||
Most of the C API is thread-safe, but there are some exceptions.
|
||||
|
||||
* **Struct Fields**: Accessing fields in Python C API objects or structs
|
||||
directly is not thread-safe if the field may be concurrently modified.
|
||||
* **Macros**: Accessor macros like :c:macro:`PyList_GET_ITEM` and
|
||||
:c:macro:`PyList_SET_ITEM` do not perform any error checking or locking.
|
||||
These macros are not thread-safe if the container object may be modified
|
||||
concurrently.
|
||||
* **Borrowed References**: C API functions that return
|
||||
:term:`borrowed references <borrowed reference>` may not be thread-safe if
|
||||
the containing object is modified concurrently. See the section on
|
||||
:ref:`borrowed references <borrowed-references>` for more information.
|
||||
|
||||
|
||||
Container Thread Safety
|
||||
.......................
|
||||
|
||||
Containers like :c:struct:`PyListObject`,
|
||||
:c:struct:`PyDictObject`, and :c:struct:`PySetObject` perform internal locking
|
||||
in the free-threaded build. For example, the :c:func:`PyList_Append` will
|
||||
lock the list before appending an item.
|
||||
|
||||
|
||||
Borrowed References
|
||||
===================
|
||||
|
||||
.. _borrowed-references:
|
||||
|
||||
Some C API functions return :term:`borrowed references <borrowed reference>`.
|
||||
These APIs are not thread-safe if the containing object is modified
|
||||
concurrently. For example, it's not safe to use :c:func:`PyList_GetItem`
|
||||
if the list may be modified concurrently.
|
||||
|
||||
The following table lists some borrowed reference APIs and their replacements
|
||||
that return :term:`strong references <strong reference>`.
|
||||
|
||||
+-----------------------------------+-----------------------------------+
|
||||
| Borrowed reference API | Strong reference API |
|
||||
+===================================+===================================+
|
||||
| :c:func:`PyList_GetItem` | :c:func:`PyList_GetItemRef` |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
| :c:func:`PyDict_GetItem` | :c:func:`PyDict_GetItemRef` |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
| :c:func:`PyDict_GetItemWithError` | :c:func:`PyDict_GetItemRef` |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
| :c:func:`PyDict_GetItemString` | :c:func:`PyDict_GetItemStringRef` |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
| :c:func:`PyDict_SetDefault` | :c:func:`PyDict_SetDefaultRef` |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
| :c:func:`PyDict_Next` | no direct replacement |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
| :c:func:`PyWeakref_GetObject` | :c:func:`PyWeakref_GetRef` |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
| :c:func:`PyWeakref_GET_OBJECT` | :c:func:`PyWeakref_GetRef` |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
| :c:func:`PyImport_AddModule` | :c:func:`PyImport_AddModuleRef` |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
|
||||
Not all APIs that return borrowed references are problematic. For
|
||||
example, :c:func:`PyTuple_GetItem` is safe because tuples are immutable.
|
||||
Similarly, not all uses of the above APIs are problematic. For example,
|
||||
:c:func:`PyDict_GetItem` is often used for parsing keyword argument
|
||||
dictionaries in function calls; those keyword argument dictionaries are
|
||||
effectively private (not accessible by other threads), so using borrowed
|
||||
references in that context is safe.
|
||||
|
||||
Some of these functions were added in Python 3.13. You can use the
|
||||
`pythoncapi-compat <https://github.com/python/pythoncapi-compat>`_ package
|
||||
to provide implementations of these functions for older Python versions.
|
||||
|
||||
|
||||
Memory Allocation APIs
|
||||
======================
|
||||
|
||||
Python's memory management C API provides functions in three different
|
||||
:ref:`allocation domains <allocator-domains>`: "raw", "mem", and "object".
|
||||
For thread-safety, the free-threaded build requires that only Python objects
|
||||
are allocated using the object domain, and that all Python object are
|
||||
allocated using that domain. This differes from the prior Python versions,
|
||||
where this was only a best practice and not a hard requirement.
|
||||
|
||||
.. note::
|
||||
|
||||
Search for uses of :c:func:`PyObject_Malloc` in your
|
||||
extension and check that the allocated memory is used for Python objects.
|
||||
Use :c:func:`PyMem_Malloc` to allocate buffers instead of
|
||||
:c:func:`PyObject_Malloc`.
|
||||
|
||||
|
||||
Thread State and GIL APIs
|
||||
=========================
|
||||
|
||||
Python provides a set of functions and macros to manage thread state and the
|
||||
GIL, such as:
|
||||
|
||||
* :c:func:`PyGILState_Ensure` and :c:func:`PyGILState_Release`
|
||||
* :c:func:`PyEval_SaveThread` and :c:func:`PyEval_RestoreThread`
|
||||
* :c:macro:`Py_BEGIN_ALLOW_THREADS` and :c:macro:`Py_END_ALLOW_THREADS`
|
||||
|
||||
These functions should still be used in the free-threaded build to manage
|
||||
thread state even when the :term:`GIL` is disabled. For example, if you
|
||||
create a thread outside of Python, you must call :c:func:`PyGILState_Ensure`
|
||||
before calling into the Python API to ensure that the thread has a valid
|
||||
Python thread state.
|
||||
|
||||
You should continue to call :c:func:`PyEval_SaveThread` or
|
||||
:c:macro:`Py_BEGIN_ALLOW_THREADS` around blocking operations, such as I/O or
|
||||
lock acquisitions, to allow other threads to run the
|
||||
:term:`cyclic garbage collector <garbage collection>`.
|
||||
|
||||
|
||||
Protecting Internal Extension State
|
||||
===================================
|
||||
|
||||
Your extension may have internal state that was previously protected by the
|
||||
GIL. You may need to add locking to protect this state. The approach will
|
||||
depend on your extension, but some common patterns include:
|
||||
|
||||
* **Caches**: global caches are a common source of shared state. Consider
|
||||
using a lock to protect the cache or disabling it in the free-threaded build
|
||||
if the cache is not critical for performance.
|
||||
* **Global State**: global state may need to be protected by a lock or moved
|
||||
to thread local storage. C11 and C++11 provide the ``thread_local`` or
|
||||
``_Thread_local`` for
|
||||
`thread-local storage <https://en.cppreference.com/w/c/language/storage_duration>`_.
|
||||
|
||||
|
||||
Building Extensions for the Free-Threaded Build
|
||||
===============================================
|
||||
|
||||
C API extensions need to be built specifically for the free-threaded build.
|
||||
The wheels, shared libraries, and binaries are indicated by a ``t`` suffix.
|
||||
|
||||
* `pypa/manylinux <https://github.com/pypa/manylinux>`_ supports the
|
||||
free-threaded build, with the ``t`` suffix, such as ``python3.13t``.
|
||||
* `pypa/cibuildwheel <https://github.com/pypa/cibuildwheel>`_ supports the
|
||||
free-threaded build if you set
|
||||
`CIBW_FREE_THREADED_SUPPORT <https://cibuildwheel.pypa.io/en/stable/options/#free-threaded-support>`_.
|
||||
|
||||
Limited C API and Stable ABI
|
||||
............................
|
||||
|
||||
The free-threaded build does not currently support the
|
||||
:ref:`Limited C API <limited-c-api>` or the stable ABI. If you use
|
||||
`setuptools <https://setuptools.pypa.io/en/latest/setuptools.html>`_ to build
|
||||
your extension and currently set ``py_limited_api=True`` you can use
|
||||
``py_limited_api=not sysconfig.get_config_var("Py_GIL_DISABLED")`` to opt out
|
||||
of the limited API when building with the free-threaded build.
|
||||
|
||||
.. note::
|
||||
You will need to build separate wheels specifically for the free-threaded
|
||||
build. If you currently use the stable ABI, you can continue to build a
|
||||
single wheel for multiple non-free-threaded Python versions.
|
||||
|
||||
|
||||
Windows
|
||||
.......
|
||||
|
||||
Due to a limitation of the official Windows installer, you will need to
|
||||
manually define ``Py_GIL_DISABLED=1`` when building extensions from source.
|
|
@ -34,4 +34,5 @@ Currently, the HOWTOs are:
|
|||
isolating-extensions.rst
|
||||
timerfd.rst
|
||||
mro.rst
|
||||
free-threading-extensions.rst
|
||||
|
||||
|
|
Loading…
Reference in New Issue