Skip to content

Commit 18480c7

Browse files
davidhewittngoldbaumIcxolu
authored
add PyOnceLock type (#5223)
* add `PyOnceLock` type --------- Co-authored-by: Nathan Goldbaum <nathan.goldbaum@gmail.com> * Apply suggestions from code review Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Update src/impl_/exceptions.rs Co-authored-by: Nathan Goldbaum <nathan.goldbaum@gmail.com> --------- Co-authored-by: Nathan Goldbaum <nathan.goldbaum@gmail.com> Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com>
1 parent 56efcdd commit 18480c7

33 files changed

+438
-99
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ rust-version.workspace = true
2727
[dependencies]
2828
libc = "0.2.62"
2929
memoffset = "0.9"
30+
once_cell = "1.21"
3031

3132
# ffi bindings to the python interpreter, split into a separate crate so they can be used independently
3233
pyo3-ffi = { path = "pyo3-ffi", version = "=0.25.1" }
@@ -129,6 +130,7 @@ auto-initialize = []
129130
# Enables `Clone`ing references to Python objects `Py<T>` which panics if the GIL is not held.
130131
py-clone = []
131132

133+
# Adds `OnceExt` and `MutexExt` implementations to the `parking_lot` types
132134
parking_lot = ["dep:parking_lot", "lock_api"]
133135
arc_lock = ["lock_api", "lock_api/arc_lock", "parking_lot?/arc_lock"]
134136

guide/src/faq.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ Sorry that you're having trouble using PyO3. If you can't find the answer to you
44

55
## I'm experiencing deadlocks using PyO3 with `std::sync::OnceLock`, `std::sync::LazyLock`, `lazy_static`, and `once_cell`!
66

7-
`OnceLock`, `LazyLock`, and their thirdparty predecessors use blocking to ensure only one thread ever initializes them. Because the Python GIL is an additional lock this can lead to deadlocks in the following way:
7+
`OnceLock`, `LazyLock`, and their thirdparty predecessors use blocking to ensure only one thread ever initializes them. Because the Python interpreter can introduce additional locks (the Python GIL and GC can both require all other threads to pause) this can lead to deadlocks in the following way:
88

9-
1. A thread (thread A) which has acquired the Python GIL starts initialization of a `OnceLock` value.
10-
2. The initialization code calls some Python API which temporarily releases the GIL e.g. `Python::import`.
11-
3. Another thread (thread B) acquires the Python GIL and attempts to access the same `OnceLock` value.
9+
1. A thread (thread A) which is attached to the Python interpreter starts initialization of a `OnceLock` value.
10+
2. The initialization code calls some Python API which temporarily detaches from the interpreter e.g. `Python::import`.
11+
3. Another thread (thread B) attaches to the Python interpreter and attempts to access the same `OnceLock` value.
1212
4. Thread B is blocked, because it waits for `OnceLock`'s initialization to lock to release.
13-
5. Thread A is blocked, because it waits to re-acquire the GIL which thread B still holds.
13+
5. On non-free-threaded Python, thread A is now also blocked, because it waits to re-attach to the interpreter (by taking the GIL which thread B still holds).
1414
6. Deadlock.
1515

16-
PyO3 provides a struct [`GILOnceCell`] which implements a single-initialization API based on these types that relies on the GIL for locking. If the GIL is released or there is no GIL, then this type allows the initialization function to race but ensures that the data is only ever initialized once. If you need to ensure that the initialization function is called once and only once, you can make use of the [`OnceExt`] and [`OnceLockExt`] extension traits that enable using the standard library types for this purpose but provide new methods for these types that avoid the risk of deadlocking with the Python GIL. This means they can be used in place of other choices when you are experiencing the deadlock described above. See the documentation for [`GILOnceCell`] and [`OnceExt`] for further details and an example how to use them.
16+
PyO3 provides a struct [`PyOnceLock`] which implements a single-initialization API based on these types that avoids deadlocks. You can also make use of the [`OnceExt`] and [`OnceLockExt`] extension traits that enable using the standard library types for this purpose by providing new methods for these types that avoid the risk of deadlocking with the Python interpreter. This means they can be used in place of other choices when you are experiencing the deadlock described above. See the documentation for [`PyOnceLock`] and [`OnceExt`] for further details and an example how to use them.
1717

18-
[`GILOnceCell`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILOnceCell.html
18+
[`PyOnceLock`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.PyOnceLock.html
1919
[`OnceExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html
2020
[`OnceLockExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html
2121

guide/src/free-threading.md

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -285,31 +285,39 @@ the free-threaded build.
285285

286286
### Thread-safe single initialization
287287

288-
Until version 0.23, PyO3 provided only [`GILOnceCell`] to enable deadlock-free
289-
single initialization of data in contexts that might execute arbitrary Python
290-
code. While we have updated [`GILOnceCell`] to avoid thread safety issues
291-
triggered only under the free-threaded build, the design of [`GILOnceCell`] is
292-
inherently thread-unsafe, in a manner that can be problematic even in the
293-
GIL-enabled build.
294-
295-
If, for example, the function executed by [`GILOnceCell`] releases the GIL or
296-
calls code that releases the GIL, then it is possible for multiple threads to
297-
race to initialize the cell. While the cell will only ever be initialized
298-
once, it can be problematic in some contexts that [`GILOnceCell`] does not block
299-
like the standard library [`OnceLock`].
300-
301-
In cases where the initialization function must run exactly once, you can bring
302-
the [`OnceExt`] or [`OnceLockExt`] traits into scope. The [`OnceExt`] trait adds
288+
To initialize data exactly once, use the [`PyOnceLock`] type, which is a close equivalent
289+
to [`std::sync::OnceLock`][`OnceLock`] that also helps avoid deadlocks by detaching from
290+
the Python interpreter when threads are blocking waiting for another thread to
291+
complete intialization. If already using [`OnceLock`] and it is impractical
292+
to replace with a [`PyOnceLock`], there is the [`OnceLockExt`] extension trait
293+
which adds [`OnceLockExt::get_or_init_py_attached`] to detach from the interpreter
294+
when blocking in the same fashion as [`PyOnceLock`]. Here is an example using
295+
[`PyOnceLock`] to single-initialize a runtime cache holding a `Py<PyDict>`:
296+
297+
```rust
298+
# use pyo3::prelude::*;
299+
use pyo3::sync::PyOnceLock;
300+
use pyo3::types::PyDict;
301+
302+
let cache: PyOnceLock<Py<PyDict>> = PyOnceLock::new();
303+
304+
Python::attach(|py| {
305+
// guaranteed to be called once and only once
306+
cache.get_or_init(py, || PyDict::new(py).unbind())
307+
});
308+
```
309+
310+
In cases where a function must run exactly once, you can bring
311+
the [`OnceExt`] trait into scope. The [`OnceExt`] trait adds
303312
[`OnceExt::call_once_py_attached`] and [`OnceExt::call_once_force_py_attached`]
304313
functions to the api of `std::sync::Once`, enabling use of [`Once`] in contexts
305-
where the GIL is held. Similarly, [`OnceLockExt`] adds
306-
[`OnceLockExt::get_or_init_py_attached`]. These functions are analogous to
307-
[`Once::call_once`], [`Once::call_once_force`], and [`OnceLock::get_or_init`] except
308-
they accept a [`Python<'py>`] token in addition to an `FnOnce`. All of these
309-
functions release the GIL and re-acquire it before executing the function,
310-
avoiding deadlocks with the GIL that are possible without using the PyO3
311-
extension traits. Here is an example of how to use [`OnceExt`] to
312-
enable single-initialization of a runtime cache holding a `Py<PyDict>`.
314+
where the thread is attached to the Python interpreter. These functions are analogous to
315+
[`Once::call_once`], [`Once::call_once_force`] except they accept a [`Python<'py>`]
316+
token in addition to an `FnOnce`. All of these functions detach from the
317+
interpreter before blocking and re-attach before executing the function,
318+
avoiding deadlocks that are possible without using the PyO3
319+
extension traits. Here the same example as above built using a [`Once`] instead of a
320+
[`PyOnceLock`]:
313321

314322
```rust
315323
# use pyo3::prelude::*;
@@ -411,4 +419,5 @@ interpreter.
411419
[`Python::detach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.detach
412420
[`Python::attach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.attach
413421
[`Python<'py>`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html
422+
[`PyOnceLock`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.PyOnceLock.html
414423
[`threading`]: https://docs.python.org/3/library/threading.html

guide/src/migration.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,46 @@ For this reason we chose to rename these to more modern terminology introduced i
2121
<summary><small>Click to expand</small></summary>
2222

2323
The type alias `PyObject` (aka `Py<PyAny>`) is often confused with the identically named FFI definition `pyo3::ffi::PyObject`. For this reason we are deprecating its usage. To migrate simply replace its usage by the target type `Py<PyAny>`.
24+
</details>
25+
26+
### Replacement of `GILOnceCell` with `PyOnceLock`
27+
<details open>
28+
<summary><small>Click to expand</small></summary>
29+
30+
Similar to the above renaming of `Python::with_gil` and related APIs, the `GILOnceCell` type was designed for a Python interpreter which was limited by the GIL. Aside from its name, it allowed for the "once" initialization to race because the racing was mediated by the GIL and was extremely unlikely to manifest in practice.
31+
32+
With the introduction of free-threaded Python the racy initialization behavior is more likely to be problematic and so a new type `PyOnceLock` has been introduced which performs true single-initialization correctly while attached to the Python interpreter. It exposes the same API as `GILOnceCell`, so should be a drop-in replacement with the notable exception that if the racy initialization of `GILOnceCell` was inadvertently relied on (e.g. due to circular references) then the stronger once-ever guarantee of `PyOnceLock` may lead to deadlocking which requires refactoring.
33+
34+
Before:
2435

36+
```rust
37+
# #![allow(deprecated)]
38+
# use pyo3::prelude::*;
39+
# use pyo3::sync::GILOnceCell;
40+
# use pyo3::types::PyType;
41+
# fn main() -> PyResult<()> {
42+
# Python::attach(|py| {
43+
static DECIMAL_TYPE: GILOnceCell<Py<PyType>> = GILOnceCell::new();
44+
DECIMAL_TYPE.import(py, "decimal", "Decimal")?;
45+
# Ok(())
46+
# })
47+
# }
48+
```
49+
50+
After:
51+
52+
```rust
53+
# use pyo3::prelude::*;
54+
# use pyo3::sync::PyOnceLock;
55+
# use pyo3::types::PyType;
56+
# fn main() -> PyResult<()> {
57+
# Python::attach(|py| {
58+
static DECIMAL_TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
59+
DECIMAL_TYPE.import(py, "decimal", "Decimal")?;
60+
# Ok(())
61+
# })
62+
# }
63+
```
2564
</details>
2665

2766
### Deprecation of `GILProtected`
@@ -64,7 +103,7 @@ Python::attach(|py| {
64103
# })
65104
# }
66105
```
67-
</summary>
106+
</details>
68107

69108
### `PyMemoryError` now maps to `io::ErrorKind::OutOfMemory` when converted to `io::Error`
70109
<details>

newsfragments/5171.packaging.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
Update MSRV to 1.74.
2-
Drop `once_cell` dependency.

newsfragments/5223.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `PyOnceLock` type for thread-safe single-initialization.

newsfragments/5223.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Deprecate `GILOnceCell` type in favour of `PyOnceLock`.

pyo3-macros-backend/src/pyclass.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2468,12 +2468,8 @@ impl<'a> PyClassImplsBuilder<'a> {
24682468
impl #pyo3_path::impl_::pyclass::PyClassWithFreeList for #cls {
24692469
#[inline]
24702470
fn get_free_list(py: #pyo3_path::Python<'_>) -> &'static ::std::sync::Mutex<#pyo3_path::impl_::freelist::PyObjectFreeList> {
2471-
static FREELIST: #pyo3_path::sync::GILOnceCell<::std::sync::Mutex<#pyo3_path::impl_::freelist::PyObjectFreeList>> = #pyo3_path::sync::GILOnceCell::new();
2472-
// If there's a race to fill the cell, the object created
2473-
// by the losing thread will be deallocated via RAII
2474-
&FREELIST.get_or_init(py, || {
2475-
::std::sync::Mutex::new(#pyo3_path::impl_::freelist::PyObjectFreeList::with_capacity(#freelist))
2476-
})
2471+
static FREELIST: #pyo3_path::sync::PyOnceLock<::std::sync::Mutex<#pyo3_path::impl_::freelist::PyObjectFreeList>> = #pyo3_path::sync::PyOnceLock::new();
2472+
&FREELIST.get_or_init(py, || ::std::sync::Mutex::new(#pyo3_path::impl_::freelist::PyObjectFreeList::with_capacity(#freelist)))
24772473
}
24782474
}
24792475
}

src/conversions/bigdecimal.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,20 @@ use std::str::FromStr;
5454
use crate::types::PyTuple;
5555
use crate::{
5656
exceptions::PyValueError,
57-
sync::GILOnceCell,
57+
sync::PyOnceLock,
5858
types::{PyAnyMethods, PyStringMethods, PyType},
5959
Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python,
6060
};
6161
use bigdecimal::BigDecimal;
6262
use num_bigint::Sign;
6363

6464
fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
65-
static DECIMAL_CLS: GILOnceCell<Py<PyType>> = GILOnceCell::new();
65+
static DECIMAL_CLS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
6666
DECIMAL_CLS.import(py, "decimal", "Decimal")
6767
}
6868

6969
fn get_invalid_operation_error_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
70-
static INVALID_OPERATION_CLS: GILOnceCell<Py<PyType>> = GILOnceCell::new();
70+
static INVALID_OPERATION_CLS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
7171
INVALID_OPERATION_CLS.import(py, "decimal", "InvalidOperation")
7272
}
7373

src/conversions/chrono.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess};
5252
#[cfg(feature = "chrono-local")]
5353
use crate::{
5454
exceptions::PyRuntimeError,
55-
sync::GILOnceCell,
55+
sync::PyOnceLock,
5656
types::{PyString, PyStringMethods},
5757
Py,
5858
};
@@ -449,7 +449,7 @@ impl<'py> IntoPyObject<'py> for Local {
449449
type Error = PyErr;
450450

451451
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
452-
static LOCAL_TZ: GILOnceCell<Py<PyTzInfo>> = GILOnceCell::new();
452+
static LOCAL_TZ: PyOnceLock<Py<PyTzInfo>> = PyOnceLock::new();
453453
let tz = LOCAL_TZ
454454
.get_or_try_init(py, || {
455455
let iana_name = iana_time_zone::get_timezone().map_err(|e| {

0 commit comments

Comments
 (0)