-
Notifications
You must be signed in to change notification settings - Fork 211
Removing per-thread file descriptors #13
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Comments
Interesting idea. I guess why not? I wouldn't expect the kernel to care about high-performance simultaneous reading of
|
Yes, I thought about using atomic, but:
Can you elaborate? Either way we need a static, so why not make it mutable directly without One thing I am not sure about is to how process error from |
Firstly, Mutable statics vs IIRC it's impossible to |
I was more afraid of 16-bit targets, but looks like *NIXes supported by |
It depends whether the goal is to initialise at least once or exactly once; mutexes are better for the latter and the std lib is in the process of switching to the However, post-init, mutexes are not required, thus the following flow seems to be what we want:
This is a bit complex, but so is |
Because we store descriptor and forget initial I thought about a simpler algorithm: const USE_SYSCALL: usize = 1 << 0;
const USE_FD: usize = 1 << 1;
const INIT: usize = 1 << 2;
let state = STATE.load(Ordering::Acquire);
if state & USE_SYSCALL != 0 {
return use_syscall(buf)
} else if state & USE_FD != 0 {
return use_fd(buf)
}
loop {
let state = STATE.fetch_or(INIT, Ordering::SeqCst);
return if state & USE_SYSCALL != 0 {
use_syscall(buf)
} else if state & USE_FD != 0 {
use_fd(buf)
} else if state & INIT == 0 {
// this function initializes source, sets appropriate bit to 1 in the STATE
// and fills the given `buf`. If during initialization an error happened,
// it will set INIT bit to 0 and will return the error
init_and_use(buf)
} else {
// we may want to sleep a short duration of time instead,
// see: https://github.com/rust-lang/rust/issues/46774
std::thread::yield_now();
continue;
};
} I think we can change the first ordering from With this approach if several threads use uninitialized |
No, I don't think we need to worry about wasted CPU cycles too much, but using a hand-crafted spin-mutex (especially one without an RAII-style guard) feels like a step back from Rust's safety goals. BTW the first state read + if logic is redundant, which does make this a very neat algorithm. |
The same can be said about using raw file descriptors and The first read is not redundant, it uses a less restricted atomic operation, so I believe it should be more efficient. |
@newpavlov I think that first ordering has to be I have an alternative implementation which races to be the first to store a file descriptor and avoids the spin-lock problem. I think the orderings are correct but I would like someone with more experience to take a look. static STATE: AtomicUsize = AtomicUsize::new(0);
static FD: AtomicUsize = AtomicUsize::new(0);
const USE_SYSCALL: usize = 1;
const USE_FD: usize = 2;
// `STATE` never becomes uninitialised so we can use a `Relaxed` load for the fast path.
let state = STATE.load(Ordering::Relaxed);
if state & USE_SYSCALL != 0 {
return use_syscall(buf);
} else if state & USE_FD != 0 {
// `FD` is either valid or 0 as we have not synchronised with the release-store to either
// `STATE` or `FD`. If it is 0 we continue on to the slow path.
let fd = FD.load(Ordering::Relaxed);
if fd != 0 {
return use_fd(fd, buf);
}
}
// The acquire-laod synchronises with any prior release-store to `STATE` so `Relaxed` stores to
// `FD` are visible.
let state = STATE.load(Ordering::Aquire);
if state == USE_SYSCALL {
use_syscall(buf)
} else if state == USE_FD {
// The prior aquire-load of `STATE` guarantees that the preceding store to `FD` will be
// visible so we can use a `Relaxed` load.
let fd = FD.load(Ordering::Relaxed);
use_fd(fd, buf)
} else {
let syscall_available = use_syscall(buf);
if syscall_available {
// Release-store to `STATE` to synchronise with any subsequent aquire-loads of `STATE`.
STATE.store(USE_SYSCALL, Ordering::Release);
} else {
let new_fd = open_fd();
// We do not care about orderings here. Only one thread will successfully store to `FD` and
// will then release-store to `STATE` so its write to `FD` becomes visible to other
// threads. Therefore a `Relaxed` ordering is used.
let current_fd = FD.compare_and_swap(0, new_fd, Ordering::Relaxed);
if current_fd == 0 {
// We have just initialised `FD` for the first time. Update `STATE` with a
// release-store to synchronise with any subsequent aquire-loads of `STATE`.
STATE.store(USE_FD, Ordering::Release);
use_fd(new_fd, buf)
} else {
// `FD` has already been initialised by another thread. That thread will perform
// the release-store to `STATE`. Close the file descriptor we have just opened and read
// from the one stored in `FD`.
close_fd(new_fd);
use_fd(current_fd, buf)
}
}
}; |
Damn. Didn't see #25. |
Uh oh!
There was an error while loading. Please reload this page.
I think we can replace TLS for several targets by using a mutable static
RawFd
initialized viastd::sync::Once
. I think it should be safe to read from/dev/urandom
using a single file descriptor from several threads without any synchronization, but it's better to check it. A small experiment shows that reading a huge amount of random data in one thread does block other threads.The text was updated successfully, but these errors were encountered: