Skip to content

Commit

Permalink
Remove GILProtected on free-threaded build (#4504)
Browse files Browse the repository at this point in the history
* do not define GILProtected if Py_GIL_DISABLED is set

* add stub for migration guide on free-threaded support

* remove internal uses of GILProtected on gil-enabled builds as well

* add newsfragment

* flesh out documentation

* fixup migration guide examples

* simplify migration guide example
  • Loading branch information
ngoldbaum authored Sep 4, 2024
1 parent 286ddab commit 8ee5510
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 12 deletions.
74 changes: 74 additions & 0 deletions guide/src/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,80 @@ impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper {
```
</details>

### Free-threaded Python Support
<details open>
<summary><small>Click to expand</small></summary>

PyO3 0.23 introduces preliminary support for the new free-threaded build of
CPython 3.13. PyO3 features that implicitly assumed the existence of the GIL
are not exposed in the free-threaded build, since they are no longer safe.

If you make use of these features then you will need to account for the
unavailability of this API in the free-threaded build. One way to handle it is
via conditional compilation -- extensions built for the free-threaded build will
have the `Py_GIL_DISABLED` attribute defined.

### `GILProtected`

`GILProtected` 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, `std::sync::Atomic` or `std::sync::Mutex` will be sufficient. If the
locks do 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 first place.

Before:

```rust
# fn main() {
# #[cfg(not(Py_GIL_DISABLED))] {
# use pyo3::prelude::*;
use pyo3::sync::GILProtected;
use pyo3::types::{PyDict, PyNone};
use std::cell::RefCell;

static OBJECTS: GILProtected<RefCell<Vec<Py<PyDict>>>> =
GILProtected::new(RefCell::new(Vec::new()));

Python::with_gil(|py| {
// stand-in for something that executes arbitrary python code
let d = PyDict::new(py);
d.set_item(PyNone::get(py), PyNone::get(py)).unwrap();
OBJECTS.get(py).borrow_mut().push(d.unbind());
});
# }}
```

After:

```rust
# use pyo3::prelude::*;
# fn main() {
use pyo3::types::{PyDict, PyNone};
use std::sync::Mutex;

static OBJECTS: Mutex<Vec<Py<PyDict>>> = Mutex::new(Vec::new());

Python::with_gil(|py| {
// stand-in for something that executes arbitrary python code
let d = PyDict::new(py);
d.set_item(PyNone::get(py), PyNone::get(py)).unwrap();
// we're not executing python code while holding the lock, so GILProtected
// was never needed
OBJECTS.lock().unwrap().push(d.unbind());
});
# }
```

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. Python 3.13 introduces `PyMutex`, which releases
the GIL while the lock is held, so that is another option if you only need to
support newer Python versions.

</details>

## from 0.21.* to 0.22

### Deprecation of `gil-refs` feature continues
Expand Down
2 changes: 2 additions & 0 deletions newsfragments/4504.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* The `GILProtected` struct is not available on the free-threaded build of
Python 3.13.
24 changes: 13 additions & 11 deletions src/impl_/pyclass/lazy_type_object.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::{
cell::RefCell,
ffi::CStr,
marker::PhantomData,
thread::{self, ThreadId},
Expand All @@ -11,11 +10,13 @@ use crate::{
impl_::pyclass::MaybeRuntimePyMethodDef,
impl_::pymethods::PyMethodDefType,
pyclass::{create_type_object, PyClassTypeObject},
sync::{GILOnceCell, GILProtected},
sync::GILOnceCell,
types::PyType,
Bound, PyClass, PyErr, PyObject, PyResult, Python,
};

use std::sync::Mutex;

use super::PyClassItemsIter;

/// Lazy type object for PyClass.
Expand All @@ -27,7 +28,7 @@ struct LazyTypeObjectInner {
value: GILOnceCell<PyClassTypeObject>,
// Threads which have begun initialization of the `tp_dict`. Used for
// reentrant initialization detection.
initializing_threads: GILProtected<RefCell<Vec<ThreadId>>>,
initializing_threads: Mutex<Vec<ThreadId>>,
tp_dict_filled: GILOnceCell<()>,
}

Expand All @@ -38,7 +39,7 @@ impl<T> LazyTypeObject<T> {
LazyTypeObject(
LazyTypeObjectInner {
value: GILOnceCell::new(),
initializing_threads: GILProtected::new(RefCell::new(Vec::new())),
initializing_threads: Mutex::new(Vec::new()),
tp_dict_filled: GILOnceCell::new(),
},
PhantomData,
Expand Down Expand Up @@ -117,7 +118,7 @@ impl LazyTypeObjectInner {

let thread_id = thread::current().id();
{
let mut threads = self.initializing_threads.get(py).borrow_mut();
let mut threads = self.initializing_threads.lock().unwrap();
if threads.contains(&thread_id) {
// Reentrant call: just return the type object, even if the
// `tp_dict` is not filled yet.
Expand All @@ -127,20 +128,18 @@ impl LazyTypeObjectInner {
}

struct InitializationGuard<'a> {
initializing_threads: &'a GILProtected<RefCell<Vec<ThreadId>>>,
py: Python<'a>,
initializing_threads: &'a Mutex<Vec<ThreadId>>,
thread_id: ThreadId,
}
impl Drop for InitializationGuard<'_> {
fn drop(&mut self) {
let mut threads = self.initializing_threads.get(self.py).borrow_mut();
let mut threads = self.initializing_threads.lock().unwrap();
threads.retain(|id| *id != self.thread_id);
}
}

let guard = InitializationGuard {
initializing_threads: &self.initializing_threads,
py,
thread_id,
};

Expand Down Expand Up @@ -185,8 +184,11 @@ impl LazyTypeObjectInner {

// Initialization successfully complete, can clear the thread list.
// (No further calls to get_or_init() will try to init, on any thread.)
std::mem::forget(guard);
self.initializing_threads.get(py).replace(Vec::new());
let mut threads = {
drop(guard);
self.initializing_threads.lock().unwrap()
};
threads.clear();
result
});

Expand Down
10 changes: 9 additions & 1 deletion src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@
//! [PEP 703]: https://peps.python.org/pep-703/
use crate::{
types::{any::PyAnyMethods, PyString, PyType},
Bound, Py, PyResult, PyVisit, Python,
Bound, Py, PyResult, Python,
};
use std::cell::UnsafeCell;

#[cfg(not(Py_GIL_DISABLED))]
use crate::PyVisit;

/// Value with concurrent access protected by the GIL.
///
/// This is a synchronization primitive based on Python's global interpreter lock (GIL).
/// It ensures that only one thread at a time can access the inner value via shared references.
/// It can be combined with interior mutability to obtain mutable references.
///
/// This type is not defined for extensions built against the free-threaded CPython ABI.
///
/// # Example
///
/// Combining `GILProtected` with `RefCell` enables mutable access to static data:
Expand All @@ -31,10 +36,12 @@ use std::cell::UnsafeCell;
/// NUMBERS.get(py).borrow_mut().push(42);
/// });
/// ```
#[cfg(not(Py_GIL_DISABLED))]
pub struct GILProtected<T> {
value: T,
}

#[cfg(not(Py_GIL_DISABLED))]
impl<T> GILProtected<T> {
/// Place the given value under the protection of the GIL.
pub const fn new(value: T) -> Self {
Expand All @@ -52,6 +59,7 @@ impl<T> GILProtected<T> {
}
}

#[cfg(not(Py_GIL_DISABLED))]
unsafe impl<T> Sync for GILProtected<T> where T: Send {}

/// A write-once cell similar to [`once_cell::OnceCell`](https://docs.rs/once_cell/latest/once_cell/).
Expand Down

0 comments on commit 8ee5510

Please sign in to comment.