-
Notifications
You must be signed in to change notification settings - Fork 186
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
Removing per-thread file descriptors #13
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. |
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: