Skip to content

Commit

Permalink
docs: Add and fix links in free-threading guide. (#4673)
Browse files Browse the repository at this point in the history
* Add and fix links in free-threading guide.

* use PYO3_DOCS_URL instead of latest

* add more links and use link anchors

* revert change to pyclass-parameters.md
  • Loading branch information
ngoldbaum authored Nov 4, 2024
1 parent 63f2189 commit d45e0bd
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 40 deletions.
103 changes: 64 additions & 39 deletions guide/src/free-threading.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ cannot be sped up using parallelism.

The free-threaded build removes this limit on multithreaded Python scaling. This
means it's much more straightforward to achieve parallelism using the Python
`threading` module. If you have ever needed to use `multiprocessing` to achieve
a parallel speedup for some Python code, free-threading will likely allow the
use of Python threads instead for the same workflow.
[`threading`] module. If you
have ever needed to use
[`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html) to
achieve a parallel speedup for some Python code, free-threading will likely
allow the use of Python threads instead for the same workflow.

PyO3's support for free-threaded Python will enable authoring native Python
extensions that are thread-safe by construction, with much stronger safety
guarantees than C extensions. Our goal is to enable ["fearless
concurrency"](https://doc.rust-lang.org/book/ch16-00-concurrency.html) in the
native Python runtime by building on the Rust `Send` and `Sync` traits.
native Python runtime by building on the Rust [`Send` and
`Sync`](https://doc.rust-lang.org/nomicon/send-and-sync.html) traits.

This document provides advice for porting Rust code using PyO3 to run under
free-threaded Python. While many simple PyO3 uses, like defining an immutable
Expand All @@ -45,7 +48,8 @@ We are aware that there are some naming issues in the PyO3 API that make it
awkward to think about a runtime environment where there is no GIL. We plan to
change the names of these types to de-emphasize the role of the GIL in future
versions of PyO3, but for now you should remember that the use of the term `GIL`
in functions and types like `with_gil` and `GILOnceCell` is historical.
in functions and types like [`Python::with_gil`] and [`GILOnceCell`] is
historical.

Instead, you can think about whether or not a Rust thread is attached to a
Python interpreter runtime. See [PEP
Expand All @@ -64,16 +68,16 @@ The main reason for attaching to the Python runtime is to interact with Python
objects or call into the CPython C API. To interact with the Python runtime, the
thread must register itself by attaching to the interpreter runtime. If you are
not yet attached to the Python runtime, you can register the thread using the
[`Python::with_gil`] function. Threads created via the Python `threading` module
do not not need to do this, but all other OS threads that interact with the
Python runtime must explicitly attach using `with_gil` and obtain a `'py`
[`Python::with_gil`] function. Threads created via the Python [`threading`]
module do not not need to do this, but all other OS threads that interact with
the Python runtime must explicitly attach using `with_gil` and obtain a `'py`
liftime.

In the GIL-enabled build, PyO3 uses the `Python<'py>` type and the `'py` lifetime
to signify that the global interpreter lock is held. In the freethreaded build,
holding a `'py` lifetime means the thread is currently attached to the Python
interpreter but other threads might be simultaneously interacting with the
Python runtime.
In the GIL-enabled build, PyO3 uses the [`Python<'py>`] type and the `'py`
lifetime to signify that the global interpreter lock is held. In the
freethreaded build, holding a `'py` lifetime means only that the thread is
currently attached to the Python interpreter -- other threads can be
simultaneously interacting with the interpreter.

Since there is no GIL in the free-threaded build, releasing the GIL for
long-running tasks is no longer necessary to ensure other threads run, but you
Expand All @@ -89,15 +93,16 @@ Data attached to `pyclass` instances is protected from concurrent access by a
`RefCell`-like pattern of runtime borrow checking. Like a `RefCell`, PyO3 will
raise exceptions (or in some cases panic) to enforce exclusive access for
mutable borrows. It was always possible to generate panics like this in PyO3 in
code that releases the GIL with `allow_threads` or caling a `pymethod` accepting
`&self` from a `&mut self` (see [the docs on interior
code that releases the GIL with [`Python::allow_threads`] or calling a python
method accepting `&self` from a `&mut self` (see [the docs on interior
mutability](./class.md#bound-and-interior-mutability),) but now in free-threaded
Python there are more opportunities to trigger these panics from Python because
there is no GIL to lock concurrent access to mutably borrowed data from Python.

The most straightforward way to trigger this problem to use the Python
`threading` module to simultaneously call a rust function that mutably borrows a
`pyclass`. For example, consider the following `PyClass` implementation:
[`threading`] module to simultaneously call a rust function that mutably borrows a
[`pyclass`]({{#PYO3_DOCS_URL}}/pyo3/attr.pyclass.html). For example,
consider the following implementation:

```
# use pyo3::prelude::*;
Expand Down Expand Up @@ -154,30 +159,30 @@ needed.

## Thread-safe single initialization

Until version 0.23, PyO3 provided only `GILOnceCell` to enable deadlock-free
Until version 0.23, PyO3 provided only [`GILOnceCell`] to enable deadlock-free
single initialization of data in contexts that might execute arbitrary Python
code. While we have updated `GILOnceCell` to avoid thread safety issues
triggered only under the free-threaded build, the design of `GILOnceCell` is
code. While we have updated [`GILOnceCell`] to avoid thread safety issues
triggered only under the free-threaded build, the design of [`GILOnceCell`] is
inherently thread-unsafe, in a manner that can be problematic even in the
GIL-enabled build.

If, for example, the function executed by `GILOnceCell` releases the GIL or
If, for example, the function executed by [`GILOnceCell`] releases the GIL or
calls code that releases the GIL, then it is possible for multiple threads to
try to race to initialize the cell. While the cell will only ever be intialized
once, it can be problematic in some contexts that `GILOnceCell` does not block
like the standard library `OnceLock`.
once, it can be problematic in some contexts that [`GILOnceCell`] does not block
like the standard library [`OnceLock`].

In cases where the initialization function must run exactly once, you can bring
the `OnceExt` or `OnceLockExt` traits into scope. The `OnceExt` trait adds
`OnceExt::call_once_py_attached` and `OnceExt::call_once_force_py_attached`
functions to the api of `std::sync::Once`, enabling use of `Once` in contexts
where the GIL is held. Similarly, `OnceLockExt` adds
`OnceLockExt::get_or_init_py_attached`. These functions are analogous to
`Once::call_once`, `Once::call_once_force`, and `OnceLock::get_or_init` except
they accept a `Python<'py>` token in addition to an `FnOnce`. All of these
the [`OnceExt`] or [`OnceLockExt`] traits into scope. The [`OnceExt`] trait adds
[`OnceExt::call_once_py_attached`] and [`OnceExt::call_once_force_py_attached`]
functions to the api of `std::sync::Once`, enabling use of [`Once`] in contexts
where the GIL is held. Similarly, [`OnceLockExt`] adds
[`OnceLockExt::get_or_init_py_attached`]. These functions are analogous to
[`Once::call_once`], [`Once::call_once_force`], and [`OnceLock::get_or_init`] except
they accept a [`Python<'py>`] token in addition to an `FnOnce`. All of these
functions release the GIL and re-acquire it before executing the function,
avoiding deadlocks with the GIL that are possible without using the PyO3
extension traits. Here is an example of how to use `OnceExt` to
extension traits. Here is an example of how to use [`OnceExt`] to
enable single-initialization of a runtime cache holding a `Py<PyDict>`.

```rust
Expand Down Expand Up @@ -208,11 +213,13 @@ Python::with_gil(|py| {

## `GILProtected` is not exposed

`GILProtected` is a PyO3 type that allows mutable access to static data by
[`GILProtected`] is a PyO3 type that allows mutable access to static data by
leveraging the GIL to lock concurrent access from other threads. In
free-threaded Python there is no GIL, so you will need to replace this type with
some other form of locking. In many cases, a type from `std::sync::atomic` or
a `std::sync::Mutex` will be sufficient.
some other form of locking. In many cases, a type from
[`std::sync::atomic`](https://doc.rust-lang.org/std/sync/atomic/) or a
[`std::sync::Mutex`](https://doc.rust-lang.org/std/sync/struct.Mutex.html) will
be sufficient.

Before:

Expand Down Expand Up @@ -258,10 +265,28 @@ Python::with_gil(|py| {
```

If you are executing arbitrary Python code while holding the lock, then you will
need to use conditional compilation to use `GILProtected` on GIL-enabled Python
builds and mutexes otherwise. If your use of `GILProtected` does not guard the
need to use conditional compilation to use [`GILProtected`] on GIL-enabled Python
builds and mutexes otherwise. If your use of [`GILProtected`] does not guard the
execution of arbitrary Python code or use of the CPython C API, then conditional
compilation is likely unnecessary since `GILProtected` was not needed in the
compilation is likely unnecessary since [`GILProtected`] was not needed in the
first place and instead Rust mutexes or atomics should be preferred. Python 3.13
introduces `PyMutex`, which releases the GIL while the waiting for the lock, so
that is another option if you only need to support newer Python versions.
introduces [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex),
which releases the GIL while the waiting for the lock, so that is another option
if you only need to support newer Python versions.

[`GILOnceCell`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILOnceCell.html
[`GILProtected]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILProtected.html
[`Once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html
[`Once::call_once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#tymethod.call_once
[`Once::call_once_force`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#tymethod.call_once_force
[`OnceExt]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html
[`OnceExt::call_once_py_attached`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html#tymethod.call_once_py_attached
[`OnceExt::call_once_force_py_attached`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html#tymethod.call_once_force_py_attached
[`OnceLockExt]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html
[`OnceLockExt::get_or_init_py_attached]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html#tymethod.get_or_init_py_attached
[`OnceLock`]: https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html
[`OnceLock::get_or_init`]: https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html#tymethod.get_or_init
[`Python::allow_threads`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.allow_threads
[`Python::with_gil`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.with_gil
[`Python<'py>`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html
[`threading`]: https://docs.python.org/3/library/threading.html
2 changes: 1 addition & 1 deletion guide/src/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,7 @@ Python::with_gil(|py| {
});
```

Furthermore, `Python::acquire_gil` provides ownership of a `GILGuard` which can be freely stored and passed around. This is usually not helpful as it may keep the lock held for a long time thereby blocking progress in other parts of the program. Due to the generative lifetime attached to the GIL token supplied by `Python::with_gil`, the problem is avoided as the GIL token can only be passed down the call chain. Often, this issue can also be avoided entirely as any GIL-bound reference `&'py PyAny` implies access to a GIL token `Python<'py>` via the [`PyAny::py`](https://docs.rs/pyo3/latest/pyo3/types/struct.PyAny.html#method.py) method.
Furthermore, `Python::acquire_gil` provides ownership of a `GILGuard` which can be freely stored and passed around. This is usually not helpful as it may keep the lock held for a long time thereby blocking progress in other parts of the program. Due to the generative lifetime attached to the GIL token supplied by `Python::with_gil`, the problem is avoided as the GIL token can only be passed down the call chain. Often, this issue can also be avoided entirely as any GIL-bound reference `&'py PyAny` implies access to a GIL token `Python<'py>` via the [`PyAny::py`](https://docs.rs/pyo3/0.22.5/pyo3/types/struct.PyAny.html#method.py) method.
</details>

## from 0.17.* to 0.18
Expand Down

0 comments on commit d45e0bd

Please sign in to comment.