-
Notifications
You must be signed in to change notification settings - Fork 766
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
make GILOnceCell
threadsafe
#4512
Conversation
c4c5dc3
to
ddffef3
Compare
A few questions:
|
Ok, on deeper review, I believe there's a difference between this behavior and When This actually addresses both of my questions. However, I think it makes the documentation a bit misleading: the deadlock prevention here has nothing to do with |
Yes exactly, this change preserves the existing runtime semantics while removing reliance on the GIL. Agreed the documentation is now out of date, will correct that. The name is also unfortunate now that there is no reliance on the GIL. I think that multiple concurrent calls to A possible alternative is to disable |
Actually, I started writing Instead, I wonder if an extension trait to add a helper method to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In init
, there are two lines:
let value = f()?;
let _ = self.set(py, value);
You can get behavior that's closer to the GIL build by wrapping the two lines in a Py_BEGIN/END_CRITICAL_SECTION
. Basically, for some (but not all) implementations of f()
, the GIL ensured that f()
was only called once. A Py_BEGIN/END_CRITICAL_SECTION
should ensure those same f
's are called only once without the risk of introducing a deadlock.
Thanks @colesbury Ref your comment in the other thread:
I took a look at translating Either option makes me a bit uneasy so I might just prefer to merge this PR without the critical section and add that improvement later. |
Ok, I think this is now ready for review. I've updated the documentation to no longer state that the implementation no longer depends on the GIL, while still describing how the interaction between them works. |
It won't crash, but it's the kind of thing that might break in the future. It's probably safer to at least use
That makes sense to me. We should also work on exposing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not enough of a rust expert to reason through the safety implications, but I have some minor doc suggestions and I carefully read through the code and didn't spot any issues.
// inside the `call_once_force` closure. | ||
unsafe { | ||
// `.take().unwrap()` will never panic | ||
(*self.data.get()).write(value.take().unwrap()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
neat, I'd never seen this pattern before, it took me a few minutes to puzzle out but it's a nice pattern only consuming the value
if it's actually written.
/// let cell = GILOnceCell::new(); | ||
/// { | ||
/// let s = String::new(); | ||
/// let _ = Python::with_gil(|py| cell.set(py,A(&s))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the builds is failing here:
error[E0597]: `s` does not live long enough
--> src/sync.rs:142:50
|
24 | let s = String::new();
| - binding `s` declared here
25 | let _ = Python::with_gil(|py| cell.set(py,A(&s)));
| ---- ^ borrowed value does not live long enough
| |
| value captured here
26 | }
| - `s` dropped here while still borrowed
27 | } _doctest_main_src_sync_rs_121_0() }
| - borrow might be used here, when `cell` is dropped and runs the `Drop` code for type `GILOnceCell`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah my mistake, this is meant to be marked compile_fail
as a UI test that the type behaves properly (imported from std
).
Co-authored-by: Nathan Goldbaum <nathan.goldbaum@gmail.com>
Does anyone else want to review or add opinions to this before we merge this? I think this is probably the next big step in getting the freethreaded support working for 0.23 |
Get and set still take a Python but don't need it, should we remove it? |
Great question. I opted to leave them to avoid the breaking change, especially if we think that in the long run we might remove this type. Perhaps we punt on that for now and can always remove them in a future release? |
Will proceed to merge this, thanks all for the reviews and feedback! |
The failure seems flaky (and I can't reproduce locally, so I'm going to retry this one last time...) |
This changes
GILOnceCell
to be thread-safe. I do this by adding astd::sync::Once
to theGILOnceCell
, which blocks multiple writers from concurrently writing to the GIL (this is almost exactly howstd::sync::OnceLock
works).This comes at making accesses to the
GILOnceCell
cost an atomic load. I do this on all builds for simplicity of our implementation, so there is a bit of slowdown on the non-free threaded builds, but I don't think this will be catastrophic. I also think it's better to make the performance characteristics the same for consistency.I considered using a Python critical section here instead of the
Once
, but I came to realise that was not necessary for the short-lived lock around the write.cc @ngoldbaum
(cc @colesbury)