-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Implement reentrance detection for std::sync::Once #72311
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @sfackler (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see the contribution instructions for more information. |
Spinning unconditionally is risky on operating systems with pre-emptive schedulers even if the critical section is short. Admittedly |
I would like it to remain lock-free as well but I don't think it's possible. The main hurdle is that the
Therefore it must be kept on the stack of the initializing thread (or on the heap, but the initializing thread would own it anyway, so lifetime is the same). So the Now we need to avoid I choose a spin-lock because it happens to be encodable in the An alternative way is to use a global mutex to protect it. BTW, some background knowledge about how most C++ runtime implements lazily initialized function-local static variables: the "once guard" is only required to hold the state. A global reentrant mutex mandates that only one thread may run initializers at one moment (even for different variables). This approach is very simple and probably requires no unsafe code to implement in Rust. It disallows multiple threads from initializing different lazy variables but it can thus detect all possible deadlocks. |
I also don't have a good solution for this. Personally, I would not choose to sacrifice lock-freedom, especially when the new lock is a spinlock, to be able to detect reentrant |
TBH I wouldn't be too worried about performance of having a lock:
So the only thing to be worried is the spinlock. It can always be switched to a global mutex if that's the consensus. |
For passers-by, here's a blog post that explains the problems with user-space spin-locks. That one is Rust-specific, but many other sources come to the same conclusion. Of course, triggering the pathological behavior is more difficult in this case since it can only happen during initialization.
|
There's another strategy which does not require any additional locking:
Effectively we are walking up the call-stack to check if the same stack frame already exists further up. This only affects the cold path. |
cc @matklad, because the implementation is relevant to |
r? @dtolnay |
I am not immediately sold on this change, but let me check if others feel we should do this. @rfcbot poll libs Make std::sync::Once panic instead of deadlock on re-entrant initialization at the expense of complicating the implementation use std::sync::Once;
static ONCE: Once = Once::new();
fn main() {
ONCE.call_once(|| ONCE.call_once(|| {}));
} thread 'main' panicked at 'Once instance cannot be recursively initialized', src/libstd/sync/once.rs:417:39 |
Team member @dtolnay has asked teams: T-libs, for consensus on:
|
I think its a net good to turn programmer errors from deadlocks into panics, but I'd want to see concretely the other side of this trade off. It seems like the thread hasn't landed on a consensus for the best implementation. Once its clear what the trade off is I think we should consider if this change is worth it. |
☔ The latest upstream changes (presumably #74468) made this pull request unmergeable. Please resolve the merge conflicts. |
This is right now tagged as waiting on team |
Rebased to solve conflicts from #72414. This PR now automatically detects recursive initialization of #![feature(once_cell)]
use std::lazy::SyncLazy;
static A: SyncLazy<u32> = SyncLazy::new(|| *A);
fn main() {
println!("{}", *A);
} gives
|
☔ The latest upstream changes (presumably #73265) made this pull request unmergeable. Please resolve the merge conflicts. |
The old implementation uses lock-free linked list. This new implementation uses spin-lock so it will allow extra auxillary information to be passed from the running thread to other waiting threads. Using spin-lock may sounds horrible but it totally fine in this scenario: the critical region protected by this is very short (just adding a node to the linked list); and also this is only executed on the cold path (only if Once isn't already completed).
Let the fast path (COMPLETE) to use the constant 0. This may only reduces code sizes by 1 byte on x86/64 but it can reduce an instruction on RISC ISAs.
Created a thread on zulip to discuss this further. |
I think that we definitely don't want to introduce spin-locking in the standard library. Have you considered trying to implement re-entrance detection on |
@nbdd0121 any updates on this? |
@nbdd0121 thanks for taking the time to contribute. I have to close this due to inactivity. If you wish and you have the time you can open a new PR with these changes and we'll take it from there. Thanks |
No description provided.