4 ms·
Concurrency/parallelism is sometimes described as "abstraction piercing", i.e. you can't tell if something is safe for use in a concurrent code by just looking
by dbaupp 11y ago
Concurrency/parallelism is sometimes described as "abstraction piercing", i.e. you can't tell if something is safe for use in a concurrent code by just looking at its interface, the internals of the type and the implementation details of its functionality matter.
One way to handle this is to have each type explicitly opt-in to saying they're concurrent-safe, but this is fairly annoying, as it requires a lot of boilerplate. Boilerplate that is mostly pointless in Rust, because its rules mean most types people define will be safe, and most types that aren't safe will be built out of existing types that aren't safe. OIBITs are proxies for "safe", and are designed to strict a balance, by basically following that rule (types made out of types implementing an OIBIT automatically impl that OIBIT too).
Moving on to your specific suggestion: RefCell isn't the only way to not run destructors, for instance, one can create ownership cycles with Mutex and RWLock in a similar way, and, more broadly, threads that dead lock (or live lock) will leak their resources, as would calling std::process::exit. There's also weirder/wilder things like the following: suppose Queue<T> is a multiple-producer multiple-consumer queue type, where any instance can act as both a producer and a consumer (typical for such types), then the following code creates an ownership cycle within the queue, and so won't run the destructor of X:
struct Msg {
x: X,
y: Queue<Msg>
}
let q: Queue<Msg> = ...;
let q2 = q.clone(); // another handle to the same queue
q.send(Msg { x: ..., y: q2 });
drop(q);
Now, sure you could mark all these things unsafe, but I think that starts to make things very inconvenient, and, somewhat, weaken what `unsafe` means (it normalise its use since those things are typically totally fine). (Also, those examples are almost surely not exhaustive.)
- cousin_it 11y agoInteresting! Do you mind if I ask you a few newbie questions? 1) After some soul searching, it seems like the really important invariant to me is that all alive objects should be reachable from the stack(s) at any given moment. So I'm kind of okay with failing to run destructors due to deadlock or process exit, but not okay with leaking a cycle. Is that a sensible distinction, or is the situation even more subtle? 2) How do I make a cycle with Mutex and RWLock? 3) I don't completely understand the queue example, what exactly is the magic that allows self-reference?
- dbaupp 11y ago> 1) After some soul searching, it seems like the really important invariant to me is that all alive objects should be reachable from the stack(s) at any given moment. So I'm kind of okay with failing to run destructors due to deadlock or process exit, but not okay with leaking a cycle. Is that a sensible distinction, or is the situation even more subtle? I think that's a reasonable distinction, but, at least for deadlocks, I'm not sure it is enough to have all the guarantees one might want (particularly around multithreading/scoping). Then again, it might be. > 2) How do I make a cycle with Mutex and RWLock? The same as a RefCell, just call `.lock().unwrap()` and `.write().unwrap()` instead of `.borrow_mut()`. These two types are essentially the same as a RefCell, just with more synchronisation, and fewer panics. They can block and wait for another thread to finish, at the risk of deadlocks of course, while a RefCell is restricted to a single thread, so blocking would just dead lock a thread with itself, hence a panic resolves the situation better. In fact, other than this blocking/panicking distinction, a RwLock and RefCell are almost identical: .read == .borrow and .write == .borrow_mut. Here's a demo which creates cycles that contain a type that prints in the destructor (demonstrating that they don't run): https://play.rust-lang.org/?gist=6ea16b859e22f57fee14&version=stable&run=1 https://play.rust-lang.org/?gist=6ea16b859e22f57fee14&versio... > 3) I don't completely understand the queue example, what exactly is the magic that allows self-reference? This is also essentially the same as the core RefCell/Mutex example, replacing .borrow_mut()/.lock() with .send. An mpmc Queue<T> will be isomorphic to Arc<Mutex<VecDeque<T>>> (although unlikely to be implemented as such in practice). The key is that they will reference-count the book-keeping that needs to be shared among all handles (like the start/tail of the queue), which allows creating a reference cycle.