Skip to content

Commit 9582419

Browse files
committed
add sync::OnceExt trait
1 parent 5464f16 commit 9582419

File tree

3 files changed

+76
-5
lines changed

3 files changed

+76
-5
lines changed

Diff for: src/sealed.rs

+2
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,5 @@ impl Sealed for ModuleDef {}
5353

5454
impl<T: crate::type_object::PyTypeInfo> Sealed for PyNativeTypeInitializer<T> {}
5555
impl<T: crate::pyclass::PyClass> Sealed for PyClassInitializer<T> {}
56+
57+
impl Sealed for std::sync::Once {}

Diff for: src/sync.rs

+60-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@
55
//!
66
//! [PEP 703]: https://peps.python.org/pep-703/
77
use crate::{
8+
ffi,
9+
sealed::Sealed,
810
types::{any::PyAnyMethods, PyAny, PyString},
911
Bound, Py, PyResult, PyTypeCheck, Python,
1012
};
11-
use std::{cell::UnsafeCell, marker::PhantomData, mem::MaybeUninit, sync::Once};
13+
use std::{
14+
cell::UnsafeCell,
15+
marker::PhantomData,
16+
mem::MaybeUninit,
17+
sync::{Once, OnceState},
18+
};
1219

1320
#[cfg(not(Py_GIL_DISABLED))]
1421
use crate::PyVisit;
@@ -473,6 +480,58 @@ where
473480
}
474481
}
475482

483+
/// Helper trait for `Once` to help avoid deadlocking when using a `Once` when attached to a
484+
/// Python thread.
485+
pub trait OnceExt: Sealed {
486+
/// Similar to [`call_once`][Once::call_once], but releases the Python GIL temporarily
487+
/// if blocking on another thread currently calling this `Once`.
488+
fn call_once_py_attached(&self, py: Python<'_>, f: impl FnOnce());
489+
490+
/// Similar to [`call_once_force`][Once::call_once_force], but releases the Python GIL
491+
/// temporarily if blocking on another thread currently calling this `Once`.
492+
fn call_once_force_py_attached(&self, py: Python<'_>, f: impl FnOnce(&OnceState));
493+
}
494+
495+
impl OnceExt for Once {
496+
fn call_once_py_attached(&self, _py: Python<'_>, f: impl FnOnce()) {
497+
if self.is_completed() {
498+
return;
499+
}
500+
501+
// Safety: we are currently attached to the GIL, and we expect to block. We will save
502+
// the current thread state and restore it as soon as we are done blocking.
503+
let mut ts = Some(unsafe { ffi::PyEval_SaveThread() });
504+
505+
self.call_once(|| {
506+
unsafe { ffi::PyEval_RestoreThread(ts.take().unwrap()) };
507+
f();
508+
});
509+
if let Some(ts) = ts {
510+
// Some other thread filled this Once, so we need to restore the GIL state.
511+
unsafe { ffi::PyEval_RestoreThread(ts) };
512+
}
513+
}
514+
515+
fn call_once_force_py_attached(&self, _py: Python<'_>, f: impl FnOnce(&OnceState)) {
516+
if self.is_completed() {
517+
return;
518+
}
519+
520+
// Safety: we are currently attached to the GIL, and we expect to block. We will save
521+
// the current thread state and restore it as soon as we are done blocking.
522+
let mut ts = Some(unsafe { ffi::PyEval_SaveThread() });
523+
524+
self.call_once_force(|state| {
525+
unsafe { ffi::PyEval_RestoreThread(ts.take().unwrap()) };
526+
f(state);
527+
});
528+
if let Some(ts) = ts {
529+
// Some other thread filled this Once, so we need to restore the GIL state.
530+
unsafe { ffi::PyEval_RestoreThread(ts) };
531+
}
532+
}
533+
}
534+
476535
#[cfg(test)]
477536
mod tests {
478537
use super::*;

Diff for: tests/test_declarative_module.rs

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#![cfg(feature = "macros")]
22

3+
use std::sync::Once;
4+
35
use pyo3::create_exception;
46
use pyo3::exceptions::PyException;
57
use pyo3::prelude::*;
6-
use pyo3::sync::GILOnceCell;
8+
use pyo3::sync::{GILOnceCell, OnceExt};
79

810
#[path = "../src/tests/common.rs"]
911
mod common;
@@ -149,9 +151,17 @@ mod declarative_module2 {
149151

150152
fn declarative_module(py: Python<'_>) -> &Bound<'_, PyModule> {
151153
static MODULE: GILOnceCell<Py<PyModule>> = GILOnceCell::new();
152-
MODULE
153-
.get_or_init(py, || pyo3::wrap_pymodule!(declarative_module)(py))
154-
.bind(py)
154+
static ONCE: Once = Once::new();
155+
156+
// Guarantee that the module is only ever initialized once; GILOnceCell can race.
157+
// TODO: use OnceLock when MSRV >= 1.70
158+
ONCE.call_once_py_attached(py, || {
159+
MODULE
160+
.set(py, pyo3::wrap_pymodule!(declarative_module)(py))
161+
.expect("only ever set once");
162+
});
163+
164+
MODULE.get(py).expect("once is completed").bind(py)
155165
}
156166

157167
#[test]

0 commit comments

Comments
 (0)