-
-
Notifications
You must be signed in to change notification settings - Fork 32k
gh-119241: Add HOWTO for free-threaded C API extensions #119877
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
Merged
colesbury
merged 5 commits into
python:main
from
colesbury:gh-119241-capi-free-threaded-howto
Jun 18, 2024
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
2009b14
gh-119241: Add HOWTO for free-threaded C API extensions
colesbury 61e7dbc
Update Doc/howto/free-threading-extensions.rst
colesbury 5bc873a
Update docs
colesbury 0632808
Update Doc/howto/free-threading-extensions.rst
colesbury 9d7edf1
3.13b2 has a free-threaded build on macOS
colesbury File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
.. 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 freethreaded | ||
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 freethreaded 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 struct fields 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`. | ||
|
||
|
||
Limited C API | ||
============= | ||
|
||
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 currenlty set ``py_limited_api=True`` you can use | ||
colesbury marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe worth adding a note that this implies that they'll need to set up wheel builds specifically for the free-threaded build if they want to ship free-threaded binaries and otherwise rely on the limited API to only produce a single wheel per platform. |
||
|
||
|
||
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>`_. | ||
ncoghlan marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,4 +34,5 @@ Currently, the HOWTOs are: | |
isolating-extensions.rst | ||
timerfd.rst | ||
mro.rst | ||
free-threading-extensions.rst | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe clarify that you're talking about C structs provided by the CPython C API.