2 ms·
I've been writing a lot of async lock free rust. The main problem is that tokio futures are 'static, which is because of a design mistake that's baked deep int
by hedora 3y ago
I've been writing a lot of async lock free rust. The main problem is that tokio futures are 'static, which is because of a design mistake that's baked deep into the rust ecosystem: Leaking memory is 'safe'.
This implies that you can't statically guarantee that a future is cleaned up properly, which means that if you spawn some async work, something may std::mem::forget a future, and then the borrow checker won't know that the references that were transitively handed out by the future are still live.
Rather than sprinkle Arc everywhere, I just use an unsafe crate like this:
https://docs.rs/async-scoped/latest/async_scoped/ https://docs.rs/async-scoped/latest/async_scoped/
This catches 99% of the bugs I would have written in C++, so it's a reasonable compromise. There's been some work to try to implement non-'static futures in a safe way. I'm hoping it succeeds.
The other big problem with rust (but this is on the roadmap to be fixed this year) is that async trait's currently require Box'ed futures, which adds a malloc/free to function call boundaries(!!!)
As for the "just use a channel" advice: I've dealt with large codebases that are structured this way. It explodes your control flow all over the place. I think of channels as the modern equivalent of GOTO. (I do use them, but not often, and certainly not in cases where I just need to run a few things in parallel and then wait for completion.)
- SkiFire13 3y ago> which is because of a design mistake that's baked deep into the rust ecosystem: Leaking memory is 'safe'. Would you prefer not to have internal mutability, not to have `Rc`, or have them but with infectious unsafe trait bounds, or something else?
- hedora 3y agoI don’t see why it would be an either/or. For instance, Rc does not leak memory, and neither do many cases of interior mutability. If an API leaks memory, then I’d like it to be deemed unsafe. That way, leaking a future would be unsafe, so the borrow checker could infer (transitively) that freeing the future means that any references it had are now dead (as it can already infer when a synchronous function call pops returns). Am I missing something subtle? Edit: Rc with cycles would be a problem. I rarely intentionally use Rc though (certainly less often than I create a future). Edit 2: maybe an auto trait could statically disallow Rc cycles somehow?
- SkiFire13 3y ago> I don’t see why it would be an either/or. For instance, Rc does not leak memory, and neither do many cases of interior mutability. `Rc` and internal mutability together do allow creating cycles and thus leaking with only safe code. I suggest you to read https://cglab.ca/~abeinges/blah/everyone-poops/ https://cglab.ca/~abeinges/blah/everyone-poops/ if you haven't done already, it explains the historical reasons for why `std::mem::forget` was changed to be safe. > Edit 2: maybe an auto trait could statically disallow Rc cycles somehow? It could be done with an auto and implied trait that is implemented in the opposite case (the type can be safely leaked), but then all the ecosystem must be changed to use `?Leakable` and it would be a pain.
- hedora 3y agoWhat about the other way? There could be a trait that means “references have the same escape semantics as a stack frame”, perhaps called NotLeaky, and then a variant of tokio spawn could require NotLeaky, and return a non-‘static future. NotLeaky could be inferred in the same way as Send and Sync. As a bonus, high-availability systems could require NotLeaky at the top of their event loop, precluding runtime memory leaks. Edit: that wouldn’t work, since the future could be leaked by the caller… will read that reference.
- SkiFire13 3y agoThis won't work because there's nothing forcing you to pass `NotLeak` types to APIs that respect them. In other words, implementing a trait can only add capabilities to a type, not restrict it.
- kprotty 3y ago> The main problem is that tokio futures are 'static An important distinction to make is that tokio Futures aren't 'static, you can instead only spawn (take advantage of the runtime's concurrency) 'static Futures. > This implies that you can't statically guarantee that a future is cleaned up properly. Futures need to be Pin'd to be poll()'d. Any `T: !Unpin` that's pinned must eventually call Drop on it [0]. A type is `!Unpin` if it transitively contains a `PhantomPinned`. Futures generated by the compiler's `async` feature are such, and you can stick this in your own manually defined Futures. This lets you assume `mem::forget` shenanigans are UB once poll()'d and is what allows for intrusive/self-referential Future libraries [1]. The future can still be leaked from being kept alive by an Arc/Rc, but as a library developer I don't think you can/would-care-to reasonably distinguish that from normal use. [0]: https://doc.rust-lang.org/std/pin/#drop-guarantee https://doc.rust-lang.org/std/pin/#drop-guarantee [1]: https://docs.rs/futures-intrusive/latest/futures_intrusive/ https://docs.rs/futures-intrusive/latest/futures_intrusive/