Skip to content

Commit e7ec730

Browse files
davidhewittalexmejrsngoldbaum
authored
docs: extend documentation on Sync and thread-safety (#4695)
* guide: extend documentation on `Sync` and thread-safety * Update guide/src/class/thread-safety.md Co-authored-by: Alex Gaynor <[email protected]> * Apply suggestions from code review Co-authored-by: Bruno Kolenbrander <[email protected]> Co-authored-by: Nathan Goldbaum <[email protected]> * threadsafe -> thread-safe * datastructure -> data structure * fill out missing sections * remove dead paragraph * fix guide build --------- Co-authored-by: Alex Gaynor <[email protected]> Co-authored-by: Bruno Kolenbrander <[email protected]> Co-authored-by: Nathan Goldbaum <[email protected]>
1 parent 71100db commit e7ec730

File tree

10 files changed

+283
-153
lines changed

10 files changed

+283
-153
lines changed

guide/pyclass-parameters.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
| `str` | Implements `__str__` using the `Display` implementation of the underlying Rust datatype or by passing an optional format string `str="<format string>"`. *Note: The optional format string is only allowed for structs. `name` and `rename_all` are incompatible with the optional format string. Additional details can be found in the discussion on this [PR](https://github.com/PyO3/pyo3/pull/4233).* |
2323
| `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. |
2424
| <span style="white-space: pre">`text_signature = "(arg1, arg2, ...)"`</span> | Sets the text signature for the Python class' `__new__` method. |
25-
| `unsendable` | Required if your struct is not [`Send`][params-3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][params-4] with [`Arc`][params-5]. By using `unsendable`, your class will panic when accessed by another thread. Also note the Python's GC is multi-threaded and while unsendable classes will not be traversed on foreign threads to avoid UB, this can lead to memory leaks. |
25+
| `unsendable` | Required if your struct is not [`Send`][params-3]. Rather than using `unsendable`, consider implementing your struct in a thread-safe way by e.g. substituting [`Rc`][params-4] with [`Arc`][params-5]. By using `unsendable`, your class will panic when accessed by another thread. Also note the Python's GC is multi-threaded and while unsendable classes will not be traversed on foreign threads to avoid UB, this can lead to memory leaks. |
2626
| `weakref` | Allows this class to be [weakly referenceable][params-6]. |
2727

2828
All of these parameters can either be passed directly on the `#[pyclass(...)]` annotation, or as one or

guide/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [Basic object customization](class/object.md)
1616
- [Emulating numeric types](class/numeric.md)
1717
- [Emulating callable objects](class/call.md)
18+
- [Thread safety](class/thread-safety.md)
1819
- [Calling Python from Rust](python-from-rust.md)
1920
- [Python object types](types.md)
2021
- [Python exceptions](exception.md)

guide/src/class.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ The above example generates implementations for [`PyTypeInfo`] and [`PyClass`] f
7373

7474
### Restrictions
7575

76-
To integrate Rust types with Python, PyO3 needs to place some restrictions on the types which can be annotated with `#[pyclass]`. In particular, they must have no lifetime parameters, no generic parameters, and must implement `Send`. The reason for each of these is explained below.
76+
To integrate Rust types with Python, PyO3 needs to place some restrictions on the types which can be annotated with `#[pyclass]`. In particular, they must have no lifetime parameters, no generic parameters, and must be thread-safe. The reason for each of these is explained below.
7777

7878
#### No lifetime parameters
7979

@@ -119,9 +119,13 @@ create_interface!(IntClass, i64);
119119
create_interface!(FloatClass, String);
120120
```
121121

122-
#### Must be Send
122+
#### Must be thread-safe
123123

124-
Because Python objects are freely shared between threads by the Python interpreter, there is no guarantee which thread will eventually drop the object. Therefore all types annotated with `#[pyclass]` must implement `Send` (unless annotated with [`#[pyclass(unsendable)]`](#customizing-the-class)).
124+
Python objects are freely shared between threads by the Python interpreter. This means that:
125+
- Python objects may be created and destroyed by different Python threads; therefore #[pyclass]` objects must be `Send`.
126+
- Python objects may be accessed by multiple python threads simultaneously; therefore `#[pyclass]` objects must be `Sync`.
127+
128+
For now, don't worry about these requirements; simple classes will already be thread-safe. There is a [detailed discussion on thread-safety](./class/thread-safety.md) later in the guide.
125129

126130
## Constructor
127131

guide/src/class/thread-safety.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# `#[pyclass]` thread safety
2+
3+
Python objects are freely shared between threads by the Python interpreter. This means that:
4+
- there is no control which thread might eventually drop the `#[pyclass]` object, meaning `Send` is required.
5+
- multiple threads can potentially be reading the `#[pyclass]` data simultaneously, meaning `Sync` is required.
6+
7+
This section of the guide discusses various data structures which can be used to make types satisfy these requirements.
8+
9+
In special cases where it is known that your Python application is never going to use threads (this is rare!), these thread-safety requirements can be opted-out with [`#[pyclass(unsendable)]`](../class.md#customizing-the-class), at the cost of making concurrent access to the Rust data be runtime errors. This is only for very specific use cases; it is almost always better to make proper thread-safe types.
10+
11+
## Making `#[pyclass]` types thread-safe
12+
13+
The general challenge with thread-safety is to make sure that two threads cannot produce a data race, i.e. unsynchronized writes to the same data at the same time. A data race produces an unpredictable result and is forbidden by Rust.
14+
15+
By default, `#[pyclass]` employs an ["interior mutability" pattern](../class.md#bound-and-interior-mutability) to allow for either multiple `&T` references or a single exclusive `&mut T` reference to access the data. This allows for simple `#[pyclass]` types to be thread-safe automatically, at the cost of runtime checking for concurrent access. Errors will be raised if the usage overlaps.
16+
17+
For example, the below simple class is thread-safe:
18+
19+
```rust
20+
# use pyo3::prelude::*;
21+
22+
#[pyclass]
23+
struct MyClass {
24+
x: i32,
25+
y: i32,
26+
}
27+
28+
#[pymethods]
29+
impl MyClass {
30+
fn get_x(&self) -> i32 {
31+
self.x
32+
}
33+
34+
fn set_y(&mut self, value: i32) {
35+
self.y = value;
36+
}
37+
}
38+
```
39+
40+
In the above example, if calls to `get_x` and `set_y` overlap (from two different threads) then at least one of those threads will experience a runtime error indicating that the data was "already borrowed".
41+
42+
To avoid these errors, you can take control of the interior mutability yourself in one of the following ways.
43+
44+
### Using atomic data structures
45+
46+
To remove the possibility of having overlapping `&self` and `&mut self` references produce runtime errors, consider using `#[pyclass(frozen)]` and use [atomic data structures](https://doc.rust-lang.org/std/sync/atomic/) to control modifications directly.
47+
48+
For example, a thread-safe version of the above `MyClass` using atomic integers would be as follows:
49+
50+
```rust
51+
# use pyo3::prelude::*;
52+
use std::sync::atomic::{AtomicI32, Ordering};
53+
54+
#[pyclass(frozen)]
55+
struct MyClass {
56+
x: AtomicI32,
57+
y: AtomicI32,
58+
}
59+
60+
#[pymethods]
61+
impl MyClass {
62+
fn get_x(&self) -> i32 {
63+
self.x.load(Ordering::Relaxed)
64+
}
65+
66+
fn set_y(&self, value: i32) {
67+
self.y.store(value, Ordering::Relaxed)
68+
}
69+
}
70+
```
71+
72+
### Using locks
73+
74+
An alternative to atomic data structures is to use [locks](https://doc.rust-lang.org/std/sync/struct.Mutex.html) to make threads wait for access to shared data.
75+
76+
For example, a thread-safe version of the above `MyClass` using locks would be as follows:
77+
78+
```rust
79+
# use pyo3::prelude::*;
80+
use std::sync::Mutex;
81+
82+
struct MyClassInner {
83+
x: i32,
84+
y: i32,
85+
}
86+
87+
#[pyclass(frozen)]
88+
struct MyClass {
89+
inner: Mutex<MyClassInner>
90+
}
91+
92+
#[pymethods]
93+
impl MyClass {
94+
fn get_x(&self) -> i32 {
95+
self.inner.lock().expect("lock not poisoned").x
96+
}
97+
98+
fn set_y(&self, value: i32) {
99+
self.inner.lock().expect("lock not poisoned").y = value;
100+
}
101+
}
102+
```
103+
104+
### Wrapping unsynchronized data
105+
106+
In some cases, the data structures stored within a `#[pyclass]` may themselves not be thread-safe. Rust will therefore not implement `Send` and `Sync` on the `#[pyclass]` type.
107+
108+
To achieve thread-safety, a manual `Send` and `Sync` implementation is required which is `unsafe` and should only be done following careful review of the soundness of the implementation. Doing this for PyO3 types is no different than for any other Rust code, [the Rustonomicon](https://doc.rust-lang.org/nomicon/send-and-sync.html) has a great discussion on this.

guide/src/free-threading.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ annotate Python modules declared by rust code in your project to declare that
4949
they support free-threaded Python, for example by declaring the module with
5050
`#[pymodule(gil_used = false)]`.
5151

52+
More complicated `#[pyclass]` types may need to deal with thread-safety directly; there is [a dedicated section of the guide](./class/thread-safety.md) to discuss this.
53+
5254
At a low-level, annotating a module sets the `Py_MOD_GIL` slot on modules
5355
defined by an extension to `Py_MOD_GIL_NOT_USED`, which allows the interpreter
5456
to see at runtime that the author of the extension thinks the extension is

0 commit comments

Comments
 (0)