Description
use std::cell::Cell;
use std::thread;
use pyo3::prelude::*;
#[pyclass]
struct Class(Cell<u8>);
fn main() -> PyResult<()> {
Python::with_gil(|py| {
let class_1 = Py::new(py, Class(Cell::new(0)))?;
let class_2 = class_1.clone_ref(py);
let cell: &Cell<u8> = &class_1.borrow(py).0;
py.allow_threads(|| {
let t = thread::spawn(move || {
Python::with_gil(|py| {
let cell: &Cell<u8> = &class_2.borrow(py).0;
let init = cell.get();
loop {
let gotten = cell.get();
if init != gotten {
println!("concurrently modified! - {init} vs {gotten}");
break;
}
}
});
});
loop {
cell.set(cell.get().wrapping_add(1));
if t.is_finished() {
break;
}
}
t.join().unwrap();
});
PyResult::Ok(())
})?;
PyResult::Ok(())
}
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/…`
concurrently modified! - 63 vs 66
(concurrent modification of a Cell
is UB; I skipped the additional step where you could abuse this data race to cause more obvious UB such as reading memory out of bounds)
This is issue is possibly irrelevant in light of existing issues like #3640 (and fixed by #3646 which abandons Ungil
). Also, it might in principle also be fixable by making .borrow
do a dynamic check that we’re in the same thread as previous .borrow
; or by demanding Sync
for pyclass
; or letting the user choose between either of the two options (dynamic check or Sync
).
I’m raising this issue as another point of argument that nightly Ungil
isn’t as good as it claims to be, and this would need to be addressed in case the argument would win that we should keep Ungil
(with documented unsoundness warnings) for its benefits of not conservatively requiring Send
.