-
Notifications
You must be signed in to change notification settings - Fork 2.2k
docs: add documentation for mod_gil_not_used and multiple_interpreters #5659
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
Changes from 8 commits
94affdd
d993e80
ba7baf4
a33b312
0c5058a
b5bebc3
2ea2541
e06a2b5
202c792
944ab56
a0ad2fc
1e7656e
58c4e7b
ffb2e31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -153,6 +153,174 @@ following checklist. | |
within pybind11 that will throw exceptions on certain GIL handling errors | ||
(reference counting operations). | ||
|
||
Free-threading support | ||
================================================================== | ||
|
||
pybind11 supports the experimental free-threaded builds of Python versions 3.13 and 3.14. | ||
pybind11's internal data structures are thread safe. To enable your modules to be used with | ||
free-threading, pass the :class:`mod_gil_not_used` tag as the third argument to | ||
``PYBIND11_MODULE``. | ||
|
||
For example: | ||
|
||
.. code-block:: cpp | ||
:emphasize-lines: 1 | ||
|
||
PYBIND11_MODULE(example, m, py::mod_gil_not_used()) { | ||
py::class_<Animal> animal(m, "Animal"); | ||
// etc | ||
} | ||
|
||
Importantly, enabling your module to be used in free threading is also your promise that | ||
b-pass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
your code is thread safe. Modules must still be built against the Python free-threading branch to | ||
enable free-threading, even if they specify this tag. Adding this tag does not break | ||
compatibility with non-free-threaded Python. | ||
|
||
Sub-interpreter support | ||
================================================================== | ||
|
||
pybind11 supports isolated sub-interpreters, which are stable in Python 3.12+. Pybind11's | ||
b-pass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
internal data structures are sub-interpreter safe. To enable your modules to be imported in | ||
isolated sub-interpreters, pass the :func:`multiple_interpreters::per_interpreter_gil()` | ||
tag as the third or later argument to ``PYBIND11_MODULE``. | ||
|
||
For example: | ||
|
||
.. code-block:: cpp | ||
:emphasize-lines: 1 | ||
|
||
PYBIND11_MODULE(example, m, py::multiple_interpreters_per_interpreter_gil()) { | ||
b-pass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
py::class_<Animal> animal(m, "Animal"); | ||
// etc | ||
} | ||
|
||
Best Practices for Sub-interpreter Safety: | ||
|
||
b-pass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- Your initialization function will run for each interpreter that imports your module. | ||
|
||
- Never share Python objects across different sub-interpreters. | ||
|
||
- Keep state it in the interpreter's state dict if necessary. Avoid global/static state | ||
b-pass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
whenever possible. | ||
|
||
- Modules without any global/static state in their C++ code may already be sub-interpreter safe | ||
without any additional work! | ||
|
||
- Avoid trying to "cache" Python objects in C++ variables across function calls (this is an easy | ||
way to accidentally introduce sub-interpreter bugs). | ||
|
||
- While sub-interpreters each have their own GIL, there can now be multiple independent GILs in one | ||
program, so concurrent calls into a module from two different sub-interpreters are still | ||
possible. Therefore, your module still needs to consider thread safety. | ||
|
||
pybind11 also supports "legacy" sub-interpreters which shared a single global GIL. You can enable | ||
legacy-only behavior by using the :func:`multiple_interpreters::shared_gil()` tag in | ||
``PYBIND11_MODULE``. | ||
|
||
You can explicitly disable sub-interpreter support in your module by using the | ||
:func:`multiple_interpreter::not_supported()` tag. This is the default behavior if you do not | ||
henryiii marked this conversation as resolved.
Show resolved
Hide resolved
|
||
specify a multiple_interpreters tag. | ||
|
||
Concurrency and Parallelism in Python with pybind11 | ||
=================================================== | ||
|
||
b-pass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Sub-interpreter support does not imply free-threading support or vice versa. Free-threading safe | ||
modules can still have global/static state (as long as access to them is thread-safe), but | ||
sub-interpreter safe modules cannot. Likewise, sub-interpreter safe modules can still rely on the | ||
GIL, but free-threading safe modules cannot. | ||
|
||
Here is a simple example module which has a function that returns the previous value for a given | ||
key. | ||
b-pass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
.. code-block:: cpp | ||
|
||
PYBIND11_MODULE(example, m) { | ||
static py::dict mydict; | ||
m.def("get_last", [](py::object key, py::object next) { | ||
py::object old = py::none(); | ||
if (mydict.contains(key)) | ||
old = mydict[key]; | ||
mydict[key] = next; | ||
return old; | ||
}); | ||
|
||
This module is not free-threading safe because there are not locks for synchronization. It is | ||
b-pass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
relatively easy to make this free-threading safe: | ||
|
||
.. code-block:: cpp | ||
:emphasize-lines: 1,3,5 | ||
|
||
PYBIND11_MODULE(example, m, py::mod_gil_not_used()) { | ||
static py::dict mydict; | ||
static std::mutex mydict_lock; | ||
m.def("get_last", [](py::object key, py::object next) { | ||
std::lock_guard<std::mutex> guard(mydict_lock); | ||
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. Hm ... isn't this prone to dead-locking? IIUC, Assume this runs without free-threading, in other words, it runs with the GIL, then
The situation seems to be very similar to what's described under docs/advanced/deadlock.md I don't know what the situation is with free-threading, therefore I'm not sure what remedy to suggest here. To be sure what we're recommending here is correct and robust, it'd be ideal to have stress tests. ChatGPT has some suggestions: 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.
Correct. It must be built with free-threading, and there must be no modules loaded which don't support free-threading. (Loading a module without support will enable the GIL for the whole program.)
As written, the code does not have a deadlock. It doesn't ever release the GIL, and it can't be called without the GIL (all of the code is in the module's init, or within a function called from python).
Yes, if you added one line somewhere in this function, such as Since the code, as written, is correct I think the best remedy is probably just a warning to be careful about deadlocks which also references the deadlocks page. 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.
The best way for mere mortals like us to reason about this (although strictly speaking it's not "any", but "most" or Any call into the Python C API may release the GIL. (It will also re-acquire it before returning.) Please see the updated ChatGPT link for a more nuanced take. — I think ChatGPT's answer is partially besides the point I want to make here, but the conclusion is: "Careful examination of the specific API functions used is necessary to understand their behavior concerning the GIL." — That is of course completely impractical for any but the most narrowly defined conditions — which would have to include the Python version, platform, etc. — therefore the simplified rule above is usually most practical. For this PR, I believe it'll be best to omit most of this section until we have all examples backed up with comprehensive tests. Examples tend to get copy-pasted. If people copy this pattern, I'm sure many will run into deadlocks, which are often extremely difficult to debug, especially in large, complex applications. 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. Calls that release the GIL are not that common, it's things like IO. Can't we just provide a warning and leave the example? Unless you think the entire example is likely wrong, it still seems like it's useful to have examples. If someone modifies it to invalid it by adding a GIL call, that's not the example's fault (especially if there was a warning about the GIL). Traveling for PyCon, so didn't really get a chance to look over the example in detail, though. 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.
Hm ... I think it happens more than one might expect. E.g. if an object is garbage collected as a sideeffect, in the code after the mutex lock, all bets are off what that triggers. I really think it's best to omit this section for now, to not have people copy-paste an unsafe pattern. We could move it to a separate PR immediately, and work on it as we get a chance. If we don't know how to make this safe ... lot's of doubts popping up in my mind, TBH. Is it actually difficult? 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. I've not researched free-threaded python extensively, so I don't know what they actually recommend that modules do for locking. But if it is as bad as you describe, then almost no lock in a C/C++ module would be safe. (And I wonder if there are any CPython calls within pybind11 crossing lock boundaries.) I can try to contrive some code that needs a lock but doesn't make any CPython calls while holding it, it shouldn't be that difficult and all the verbiage is already written.... 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. @colesbury might have input? |
||
py::object old = py::none(); | ||
if (mydict.contains(key)) | ||
old = mydict[key]; | ||
mydict[key] = next; | ||
return old; | ||
}); | ||
} | ||
|
||
The mutex guarantees a consistent behavior from this function even when called currently from | ||
multiple threads at the same time. | ||
|
||
However, the global/static dict is not sub-interpreter safe, because python objects cannot be | ||
shared or moved between interpreters. To fix it, the state needs to be specific to each | ||
interpreter. One way to do that is by storing the state on another Python object. For this | ||
simple example, we will store it in :func:`globals`. | ||
|
||
.. code-block:: cpp | ||
:emphasize-lines: 3,4,5 | ||
|
||
PYBIND11_MODULE(example, m, py::multiple_interpreters::per_interpreter_gil()) { | ||
m.def("get_last", [](py::object key, py::object next) { | ||
if (!py::globals().contains("mydict")) | ||
py::globals()["mydict"] = py::dict(); | ||
py::dict mydict = py::globals()["mydict"]; | ||
py::object old = py::none(); | ||
if (mydict.contains(key)) | ||
old = mydict[key]; | ||
mydict[key] = next; | ||
return old; | ||
}); | ||
} | ||
|
||
This module is sub-interpreter safe, for both ``shared_gil`` ("legacy") and | ||
``per_interpreter_gil`` ("default") varieties. Multiple sub-interpreters could each call this same | ||
function concurrently from different threads. This is safe because each sub-interpreter's GIL | ||
protects it's own Python objects from concurrent access. | ||
|
||
However, the module is no longer free-threading safe, because we left out the mutex. If we put it | ||
back, then we can make a module that supports both free-threading and sub-interpreters: | ||
|
||
.. code-block:: cpp | ||
:emphasize-lines: 1,3,4 | ||
|
||
PYBIND11_MODULE(example, m, py::mod_gil_not_used(), py::multiple_interpreters::per_interpreter_gil()) { | ||
m.def("get_last", [](py::object key, py::object next) { | ||
static std::mutex mymutex; | ||
std::lock_guard<std::mutex> guard(mymutex); | ||
if (!py::globals().contains("mydict")) | ||
py::globals()["mydict"] = py::dict(); | ||
py::dict mydict = py::globals()["mydict"]; | ||
py::object old = py::none(); | ||
if (mydict.contains(key)) | ||
old = mydict[key]; | ||
mydict[key] = next; | ||
return old; | ||
}); | ||
} | ||
|
||
The module is now both sub-interpreter safe and free-threading safe. The mutex is still | ||
global/static state shared between interpreters. But the thread-safe nature of the | ||
mutex and the fact that it is not a Python object make it safe to use concurrently in | ||
sub-interpreters. However, it is a slight pessimization to do so, because the sub-interpreters | ||
could block each other unnecessarily by sharing a global mutex instead of a mutex per-interpreter. | ||
Moving the mutex into per-interpreter storage would solve this problem. It is left as an | ||
exercise for the reader. | ||
|
||
Binding sequence data types, iterators, the slicing protocol, etc. | ||
================================================================== | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.