5 ms·
It takes a while to get to it, but the point of the argument seems to be that naive Rust code can't have race conditions because naive Rust code will only assig
by ajross 4y ago
It takes a while to get to it, but the point of the argument seems to be that naive Rust code can't have race conditions because naive Rust code will only assign to memory through a mutable reference, and the borrow checker guarantees that no two functions (no matter which thread they may be running in) can have borrowed such a reference simultaneously (whether "concurrently" or not). And that's true as far as it goes, I guess.
But the flip side is that naive Rust code just isn't useful for multithreaded work and tends to require heavier abstractions (e.g. copy "commands" or whatever in and out of queues vs. just taking a lock around some shared data). I occasionally try to look at the natural way one expresses the kind of stuff we do in Zephyr in a Rust kernel, and... there's really not much there besides copious and extensive use of unsafe.
- nyanpasu64 4y agoIn my experience multithreaded Rust isn't substantially more restrictive then multithreaded C++ at writing code free of data races (although it very much is at writing single-threaded code free of use-after-free). The primary methods in Rust to share memory across threads (in my experience writing high-performance wait-free but userspace not kernel code) are custom UnsafeCell wrappers which implement Send and Sync and have safe locking mechanisms preventing data races, safe atomics, or safe mutexes.
- the_mungler 4y agoTo be fair, rust at kernel level seems to have way more unsafe code than usual, though I have no experience in that area so I can't say much. In rust you absolutely can "just take a lock around some shared data", but it is a bit different then other languages. mutexes actually contain the data they guard, making it impossible to modify unless you have the lock. There is another article about muexes by this same author if you want more details: http://cliffle.com/blog/rust-mutexes/ http://cliffle.com/blog/rust-mutexes/
- School-Cotton 4y ago> just taking a lock around some shared data Can’t you do this in Rust with std::sync::Mutex and similar?
- oconnor663 4y agoAbsolutely, and IMHO the Mutex/MutexGuard API is one of the best showcases of what Rust is capable of. (Fun fact: RwLock<T> is Sync only if T is Sync, but Mutex<T> is Sync even if T is not Sync!)
- masklinn 4y ago> Fun fact: RwLock<T> is Sync only if T is Sync, but Mutex<T> is Sync even if T is not Sync! What’s more interesting is figuring out why that is. Also why Arc<T> is Send only if T is Send.
- delian66 4y agoWhy is that?
- yakubin 4y agoMy guess is one way it could break, if it was otherwise, would be if T relied on thread local state.
- masklinn 4y agoThe alternative would be to actively undermine Rust's type system (and guarantees): - RwLock hands out multiple references (that's the point), Sync means a type can be used from multiple threads (concurrently), if RwLock<T: !Sync> was Sync it would allow multiple outstanding references for the same !Sync object, which is not legal. - Mutex, however, only hands out a single reference at a time (which is also why it can always hand out a mutable reference), meaning semantically it acts like it moves the T to the target thread then borrows it = nothing to sync, that's why Mutex<T> is Sync if T is Send. - For Arc, if it were Send without the wrapped object being Send it would allow Send-ing !Send objects: create an Arc<T>, clone it, move the clone to a second thread, drop the source, now you can try_unwrap() or drop() the clone and it'll work off of the second thread. This is a problem with threadlocal state, but also with resource affinity (e.g. on windows a lock's owner is recorded during locking, and only the owner can release the lock, there are also lots of APIs which can only work off of the main thread.), or thread-safety (Rc for instance would be completely broken if you could Send it, as the entire point is to use an unsynchronised refcount).
- aidenn0 4y ago> But the flip side is that naive Rust code just isn't useful for multithreaded work and tends to require heavier abstractions (e.g. copy "commands" or whatever in and out of queues vs. just taking a lock around some shared data). I occasionally try to look at the natural way one expresses the kind of stuff we do in Zephyr in a Rust kernel, and... there's really not much there besides copious and extensive use of unsafe. You can take a lock around some shared data; the standard library has such a thing. If the standard library lacked such a thing, then you could implement that abstraction in unsafe, but that at least narrows the amount of code you need to reason about the thread-safety for. Of course that only prevents data-races, not deadlocks and livelocks. Most code that you can't "just do" in Rust is code that really should have a lot of eyeballs on it were it implemented in some other language. Kernel code gets a lot of eyeballs, usually a lot of testing, and still ends up with fielded bugs[1]. For kernel code, I imagine Rust shines for device drivers; TFA applies well there; a multithreaded kernel could have thread-oblivious device drivers and no future "optimization" would accidentally share data between threads. There are data-structures and algorithms that put you in conflict with Rust's type system (a non-concurrency one that comes to mind is trees with parent pointers). Sometimes the solution is to pick a different data-structure, other times it is judicious use of unsafe. 1: Note that the same is true with parts of the Rust stdlib that are implemented with unsafe.
- duped 4y ago> e.g. copy "commands" or whatever in and out of queues vs. just taking a lock around some shared data struct Shared { /* ... */ } #[derive(Clone) struct Context { shared:Arc<Mutex<Shared>> } impl Context { fn do_stuff (&self) { let shared = self.shared.lock(); /* ... */ } fn do_more_stuff (&self) { let shared = self.shared.lock(); } } What's wrong with that? You have a bunch of safety guarantees here : - mutex poisonings are detected (you can use try_lock to recover from them) - the shared data isn't leaked and guaranteed to live long enough in each calling context - the Mutex implementation doesn't suck and iirc supports reentrant code without deadlocks, making it safe to use on the same thread - it's really easy to call `context.clone()` to send it wherever you want
- masklinn 4y ago> the Mutex implementation doesn't suck and iirc supports reentrant code without deadlocks I don’t think that’s true, at least in the “rust mutexes are reentrant” sense: a mutex hands out &mut refs, a reentrant mutex would mean concurrent &mut, which is illegal. However rust does allow handing out the one &mut to callees, without fear that it will be leaked and outlive the lock or anything, so this sort of “reentrancy” is fine if you split functions into lock acquisition and actual processing. IME deadlocks (and self-deadlocks) remain the biggest issue in concurrent rust. Lock ordering concerns are especially frustrating when fine-locking.
- duped 4y agoIf you do something like this #[derive(Clone)] struct S { a: Arc<Mutex<A>> } impl S { fn reentrant(&self) { let cloned = self.cloned(); let locked = self.a.lock(); if condition { cloned.reentrant(); } else { /* do stuff */ } } I believe it will not deadlock. Reentrant mutexes are pretty common implementations - is that not true in Rust right now?
- anderskaseorg 4y ago
- pornel 4y agoThat's not accurate. You can mutate via shared reference too, as long as something ensures it's still thread safe (e.g. synchronized). There's UnsafeCell type that is a deliberate loophole in immutability of types behind shared references. It's used as the basis for atomic access and mutexes.