-
Notifications
You must be signed in to change notification settings - Fork 192
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
Remove lazy_static dependancy #52
Conversation
cc @RalfJung, @eddyb -- can you please have a look at this in terms of |
also cc @alexcrichton |
Here's my basic idea for adding a one-time synchronization method to // Runs the init() function exactly once.
pub fn sync_init(&self, init: impl FnOnce() -> usize, mut wait: impl FnMut()) -> usize {
loop {
match self.0.compare_and_swap(Self::UNINIT, Self::ACTIVE, Relaxed) {
Self::UNINIT => {
let val = init();
self.0.store(val, Relaxed);
return val;
}
Self::ACTIVE => wait(),
val => return val,
}
}
} Where // The initialization is not completed.
pub const UNINIT: usize = usize::max_value();
// The initialization is currently running.
pub const ACTIVE: usize = usize::max_value() - 1; Then we can just use the fact that non-negative RawFd's will fit in the range |
Looks good! Small nitpicks:
|
@newpavlov Relaxed is correct for synchronizing atomic accesses to a single memory location, which I understand is what you are doing here. If you want to synchronize accesses to multiple memory locations (e.g. non-atomically writing data to a shared UnsafeCell and using a separate shared AtomicBool flag to tell other threads when the write is done), then you need stronger memory orderings like Acquire/Release pairs. |
Isn’t
Does this matter if we’re only using a single atomic value?
Typo, now fixed. |
Yes, but our callbacks will not contain any state, so stronger bound will work as well. It's just my preference, you can leave it as-is.
On some targets you use it together with |
Good point. I think on Linux we could just use a single |
I'm not sure if this is what you are talking about, but loads depending on relaxed loads can actually still cause problems:
Because compiler and CPU trickery can effectively transform the program into this:
(Not to mention that on the producer side, writes can also be reordered) If that kind of transformation is undesirable, as is the case when using an atomic as a "ready flag" to tell that other shared memory regions were written to, then you need Acquire when loading from |
Thanks @HadrienG2 — I believe this example posted earlier is correct however. |
@dhardy Yup, it uses stronger Acquire/Release memory orderings to avoid the aforementioned reordering effects. As pointed out by @RalfJung on that example, using Relaxed ordering on the CAS instead of AcqRel ordering would actually be fine as long as init_data() does not read shared data that is synchronized as a side-effect of using an Acquire load on the flag. |
@HadrienG2 // `a` is initialized with C1, and `b` with `C2`
let val = match a.load(Relaxed) {
C1 => {
let val = init1();
a.store(val, Relaxed);
val
}
val => val,
};
if val == 1 {
do_stuff1();
} else {
loop {
let val = match b.compare_and_swap(C2, C3, Relaxed) {
C2 => {
let val = init2();
b.store(val, Relaxed);
val
}
C3 => continue,
val => val,
};
do_stuff2(val);
}
} At the first glance it shouldn't have any problems, since |
BTW let val = self.0.load(Relaxed);
if val != Self::UNINIT && val != SELF::ACTIVE {
return val;
} |
@newpavlov IIUC x86 is strongly ordered, implying most aquire/release boundaries are unnecessary anyway. You should therefore check code for another target like ARM. In your example, I think it doesn't actually matter if |
@dhardy As a minor aside, I would advise caution against using "what the hardware does" as a reference for atomic ordering correctness, because the compiler can introduce additional reorderings on top of those that hardware performs, in a manner that can change in future releases even on a given architecture. @newpavlov Here's my understanding/analysis of the code summary that you posted: // `a` is initialized with C1
let val = match a.load(Relaxed) {
C1 => {
let val = init1();
a.store(val, Relaxed);
val
}
val => val,
}; First, a number of threads will read
if val == 1 {
do_stuff1();
} else {
/* ... */
} After this first initialization stage, any thread which reads
if val == 1 {
/* ... */
} else {
// `b` is initialized with `C2`
loop {
let val = match b.compare_and_swap(C2, C3, Relaxed) {
C2 => {
let val = init2();
b.store(val, Relaxed);
val
}
C3 => continue,
val => val,
};
do_stuff2(val);
}
} If
Hope this attempt at summarizing the underlying assumptions helps. On an unrelated note, I agree that you should probably check the value of |
1548e7f
to
3615137
Compare
Hey everyone, I've updated this PR and the description to incorporate your recommendations, and I split up the commits to make review easier. @HadrienG2 thanks so much for the rundown. I've tried to address all of your points by explictly putting your concerns in the comments. BTW 8566c39 contains the changes to
For our implementation, these parallelism assumptions are correct. Also, in the documentation on
In the documentation on
I note in the description of
Agreed on My comments on
Done, and good idea. |
LGTM :) |
Regarding CI tests for Redox we can bring them back after rust-lang/libc#1420 gets fixed. |
// multiple times. If init() returns UNINIT, future calls to unsync_init() | ||
// will always retry. This makes UNINIT ideal for representing failure. | ||
// of init(). Multiple callers can run their init() functions in parallel. | ||
// init() should always return the same value, if it succeeds. | ||
pub fn unsync_init(&self, init: impl FnOnce() -> usize) -> usize { | ||
// Relaxed ordering is fine, as we only have a single atomic variable. |
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.
This assumes that a file descriptor consists only of that integer and no other user-space data. I suppose that is accurate on Unixes?
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.
Yup this is correct, libc::open
always returns a positive int (link is to the Linux docs, but the function is a POSIX standard).
So a file descriptor will always fit in a positive int on unix
match self.0.compare_and_swap(Self::UNINIT, Self::ACTIVE, Relaxed) { | ||
Self::UNINIT => { | ||
let val = init(); | ||
self.0.store(val, Relaxed); |
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.
IMO it would be a good idea to assert!
here that init
did not return UNINIT
or ACTIVE
. This is not hot code, so always asserting seems fine.
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.
Oh, seems like the function is actually allowed to return UNINIT
to indicate "just try again"? I guess this is implied by the doc comment not ruling out that case, but might be worth calling out explicitly.
ACTIVE
can still be asserted.
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.
Oh, seems like the function is actually allowed to return
UNINIT
to indicate "just try again"? I guess this is implied by the doc comment not ruling out that case, but might be worth calling out explicitly.
This is called out, just in the docs for LazyUsize
.
// Both methods support init() "failing". If the init() method returns UNINIT,
// that value will be returned as normal, but will not be cached.
ACTIVE
can still be asserted.
Good idea. Thanks @RalfJung for taking a look at this stuff.
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 think it may be better to write something like this:
let mut val = init();
if val == Self::ACTIVE { val = Self::UNINIT }
self.0.store(val, Relaxed);
And describe this behavior in docs.
None => LazyUsize::UNINIT, | ||
}, | ||
|| unsafe { | ||
libc::usleep(1000); |
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.
Wow that's a long sleep, isn't it? A comment seems always warranted with such a magic number.
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.
Ya this sleep should probably be shorter, any recommendations? My guess is that we would want it to be longer than the normal latency for an open(2)
syscall.
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.
On my laptop libc::open
takes ~7µs and on playground ~8µs, so I think 10µs will be a good value. (it's a very coarse measurement, since it also includes time to retrieve current time)
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 sorry this is outside my range of experience.^^ 1ms just seemed like a long time. /dev/random
doesn't even do "real" I/O, so it shouldn't have huge latency.
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.
It can be a huge latency if system entropy pool has not initialized, IIRC up to several minutes.
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.
Sure, but that's not the common case.
Ideally wait
would do something like double the wait time each time around the loop, starting at e.g. 1µs and maxing out at 1s or so. But that seems like overkill.^^
@newpavlov @RalfJung this is a good discussion, but might be better had on #60 instead of this closed PR. |
EDIT1: We make the following changes:
wasm32-stdweb
's use oflazy_static
withstatic mut
andstd::sync::Once
sync_init
method toLazyUsize
complementingunsync_init
.LazyFd
abstraction aroundLazyUsize
to work withOption<i32>
values, were we either haveNone
orSome(x)
with x non-negative.LazyFd
inuse_file
, eliminating our last use oflazy_static
.EDIT2: Note that this does not try to remove all
std
dependencies fromuse_file.rs
. That is for a followup CL.