5 ms·
Could someone give a very simple, to-the-point example of a kind of concurrency bug that Rust prevents, for those of us who don't know Rust? (The author explici
by throwaway613834 9y ago
Could someone give a very simple, to-the-point example of a kind of concurrency bug that Rust prevents, for those of us who don't know Rust? (The author explicitly fails to think of any, so I'm hoping someone else can. It'd be more convincing to see one.)
EDIT: I meant a code example, not a paragraph. And I would obviously expect to see how the intended goal is achieved without the bug... otherwise it'd be trivial to prevent any bug (just make everything impossible).
- seren 9y agoThe most basic example would be two threads trying to write the same variable concurrently without synchronisation. As far as I know, as long as you don't use any unsafe code block, it is not possible, i.e. the compiler will protest loudly and the program won't compile. You are forced by the compiler to implement an explicit exclusion mechanism. The point is that kind of issue are silent bugs most of the time(up to point) in C, C++. It can work in most cases, and one day, something goes awfully wrong because the thread scheduling is slightly different than usual.
- arielb1 9y agoFor example, accidentally sharing a lock-less cache or a non-atomic reference counted pointer between threads. For example this code, which tries to send a reference-counted pointer between threads, which can cause the reference counter to become unsynchronized and random use-after-free: use std::thread; use std::rc::Rc; fn main() { let rcs = Rc::new("Hello, World!".to_string()); let thread_rcs = rcs.clone(); thread::spawn(move || { println!("{}", thread_rcs); }); } Is detected by the compiler and causes this error error[E0277]: the trait bound `std::rc::Rc<std::string::String>: std::marker::Send` is not satisfied in `[closure@src/main.rs:8:19: 10:6 thread_rcs:std::rc::Rc<std::string::String>]` --> src/main.rs:8:5 | 8 | thread::spawn(move || { | ^^^^^^^^^^^^^ `std::rc::Rc<std::string::String>` cannot be sent between threads safely | = help: within `[closure@src/main.rs:8:19: 10:6 thread_rcs:std::rc::Rc<std::string::String>]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<std::string::String>` = note: required because it appears within the type `[closure@src/main.rs:8:19: 10:6 thread_rcs:std::rc::Rc<std::string::String>]` = note: required by `std::thread::spawn`
- majewsky 9y agoMany (most?) of the non-deterministic bugs in concurrent programs arise from two threads trying to write to the same place in the same time, or one thread writing to one place while the other thread just read that place and assumed that it was going to be constant for the moment. Rust prevents that by having the concepts of "ownership" and "borrowing" built into the language. The Rust book probably explains this better than I ever could, but the basic idea is that you can only have one actor writing to a variable, OR any number of actors reading a variable. But you cannot have multiple actors having write access the same variable at the same time, or one actor writing to it while anyone has read access to it, unless you use some sort of serialization or copy-on-write mechanism.
- TheCycoONE 9y agoSure, the obvious example is your classic thead1 i++, thead2 i-- bug. In C you run each thread a large number of times in parallel and you end up with something other than 0. The solution is to use atomic operations or locks. Rust doesn't allow having two mutable references to the same memory location at the same time, so you would never encounter that.
- throwaway613834 9y ago> Rust doesn't allow having two mutable references to the same memory location at the same time, so you would never encounter that. Wouldn't that make a whole class of efficient algorithms impossible though? Like let's say you have an std::list<T> and you want a sorted "view" of the elements. In C++ you'd create an array of pointers and then sort it, and after that you can just modify whatever each slot points to. In Rust you... can't do that because they'd all need to be read-only? I don't imagine you can turn the read-only to read-write on a whim (otherwise if you do this with two of them how the hell would the compiler figure out if two of them point to the same slot?) so the whole thing is just impossible?
- icen 9y agoThere's always the `unsafe` escape hatch - if you need to potentially alias mutable pointers, you can (via using the 'raw pointer' types `const T` and `mut T`). You can only do this in an `unsafe` block. If you get a bug caused by aliased mutation, then you know exactly where it comes from.
- rbehrends 9y ago> Wouldn't that make a whole class of efficient algorithms impossible though? Correct. What it does is prevent data races; this is per se nothing new, but was done as early as the 70s [1]. There've been various and sundry approaches to the same problem over the years (the 90s and early aughts produced a lot of research in this area). In a way, it is frustrating that so few programming languages offer prevention of data races, so I'm grateful that Rust pushes that; but in another way, it is understandable, because all such mechanisms restrict what you can do, often in undesirable ways. Google "benign data races", for example: data races can be purposely exploited for more efficient implementations. The other problem is that data races are really the easy part to solve about concurrency. The hard parts are general race conditions, non-determinism/causality, liveness properties (such as freedom from deadlocks and starvation), and performance. Performance is one of the trickier aspects: in a shared memory system, performance is primarily a matter of reducing contention. But virtually any system that relies on some form of mutual exclusion to prevent data races introduces contention, and you have a constant tug of war between the two concerns. Particularly frustrating is that all of these issues are what software engineers call "cross-cutting concerns" [2], i.e. things that cannot easily be modularized. For example, performance may suffer from a serialization bottleneck in a module's implementation where you cannot avoid that bottleneck without exposing the module's internals. [1] https://en.wikipedia.org/wiki/Concurrent_Pascal https://en.wikipedia.org/wiki/Concurrent_Pascal [2] https://en.wikipedia.org/wiki/Cross-cutting_concern https://en.wikipedia.org/wiki/Cross-cutting_concern
- maffydub 9y agoBorrowing from https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.h... (linked to from the original post), the following code tries to access a lock-protected vector. fn use_lock(mutex: &Mutex<Vec<i32>>) { let vec = { // acquire the lock let mut guard = lock(mutex); // attempt to return a borrow of the data access(&mut guard) // guard is destroyed here, releasing the lock }; // attempt to access the data outside of the lock. vec.push(3); } It doesn't compile because the lock is not held long enough. error: `guard` does not live long enough access(&mut guard) ^~~~~ There are several more examples in that article, but you can read them there rather than here!
- throwaway613834 9y agoThanks (upvoted)! However how in the world would the compiler know if the mutex belongs to the same vector you are accessing? (Unless you're saying the mutex wraps the vector implying that each vector can have one corresponding mutex at most? Which seems quite limiting?)
- maffydub 9y agoThanks! Yes, when you construct a mutex, you give it ownership of the vector you want it to protect. Due to the way Rust works, once you've given ownership to something else, you can no longer access it yourself. The only way you can get access to the data again is to "borrow" a reference to it, but this borrow has a "lifetime", which is tied to the period for which you're holding the mutex. This is how the compiler can spot that you've tried to access the vector after the mutex has been released. As you say, this means that each vector can only have one mutex wrapping it - at least for this implementation of mutex, known as std::sync::Mutex ( https://doc.rust-lang.org/std/sync/struct.Mutex.html https://doc.rust-lang.org/std/sync/struct.Mutex.html). However, if you're looking for something like a read/write lock, Rust supports that too via std::sync::RwLock - see https://doc.rust-lang.org/std/sync/struct.RwLock.html https://doc.rust-lang.org/std/sync/struct.RwLock.html for more details. Obviously, the example uses a vector, but Rust has a pretty strong system of generics, so your mutex (or read/write lock) can wrap pretty much any type, e.g. a struct you've defined. Hope that makes sense - this was one of the areas of Rust that impressed me most!