|
| 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. |
0 commit comments