Live data from Hacker News

Why asynchronous Rust doesn't work

theta.eu.org

121–130 of 499 posts

Re: Why asynchronous Rust doesn't work

#122
post #58

Earlier quoted context omitted.

Why is it a problem that custom executors are difficult to implement? Most other languages that I'm aware of they wouldn't even be possible to implement.

It's not really a problem worth caring about. Just if you wanted to gripe about the complexity of async Rust it's the most obvious to me. "But pretty much no one even lets you do that" is a great counter argument. But, it is kind of an anti feature. It's incredible that Rust allows custom async executors and the surface area for them is tiny! That said, it's kind of black magic, even for Rust. I'm willing to bet ther…

Steve Klabnik did a talk about async/await and building your own executor: https://www.infoq.com/presentations/rust-async-await/

Might be worth a listen if you are intent on building your own!

Re: Why asynchronous Rust doesn't work

#124
> Was spinning up a bunch of OS threads not an acceptable solution for the majority of situations? Could we have explored solutions more like Go, where a language-provided runtime makes blocking more of an acceptable thing to do?

I think this is the real fundamental disagreement here (well, at least with the async stuff). (And the comments slightly earlier, regarding "the color of your function".)

The way I think about the various async stories out there (like, all of them, Go's, Rust's, Python's, C w/ OS threads), is that they boil down to roughly three or so primitives: (I haven't formalized this … and someday I should probably write a blog post on it, so it's not going to be perfect.)

  1. separate executions of code (threads, green or not, goroutines, etc.)
  2. selection (the ability to block on **multiple** threads, simultaneously,
     but return after *one* is finished or ready. It's not join().)
  3. cancellation (the ability to interrupt and cancel a thread)
"Was […] OS threads not an acceptable solution?" No: the selection story is terrible (without involving epoll. You'll pretty much have to build this out around conditions, and it's painful), and the cancellation story is close to non-existent (you have to somehow transmit to the thread that it should cancel, and then that thread needs to obey it). In combination, some things are outright impossible: if you're blocking on a socket recv in a thread, how do you get notified if you were cancelled? (You literally can't, with the primitives that most OSes offer, without resorting to hacks, or epoll.) (Some OSes have calls to kill threads. They are basically one-way tickets to UB.)

Golang's solutions get closer, but I don't think they're appropriate for a systems language that cares about low-level performance & memory layout to the degree that Rust does.¹ But even golang struggles here: the cancellation story is missing. Golang's approach here is retroactive: you have a context object, and you must thread it through function calls to anywhere it might be required. If your API didn't think to do that, you're SOL as a consumer, but worse, even as a coder, adding it is going to be a right PITA.

This isn't to say Rust's story is perfect here, either: it is tempting to think that dropping a future is cancellation, and it is very, very close (it provides a decent default). But sometimes cancellation also needs to propagate across an I/O boundary (usually, across a network socket to a server, to cancel a request) and those are async. (I think there is some work being done in Rust with asynchronous drops.)

These problems show up in JavaScript — whose Promise lacks cancellation — and Python (which has cancellation via raising CancelledError; also, Python really, IMO, messed up the terminology. Between futures, coroutines, tasks, and awaitables, there's ~2 real types (futures and tasks), and the rest are … IDK, weird distinctions that are hard to keep straight.)

¹Now, this is the heart of the other part of the article, the half about closures and fn types. Rust cares about ownership (as it allows programs to avoid not just memory issues, but a whole host of bugs resulting from ownership confusion that happen even in memory-safe languages like Python) and it cares about the performance implications of things like dynamic dispatch, or separate allocations for the data associated with the callback that you want to pass. And between trait types & generics, it lets you choose, but that does result in some verbosity. You can also choose Arc, but some of us don't want that in all cases.

(I don't agree with the "color of your function" argument/article, either: async functions in Rust are fundamentally still functions, they just return a Future instead of a T. That distinction is important; we're not just after the T, and the future is something we're going to block/wait on; it is its own type, with its own operations — specifically, and more or less, the ones above! —, that are crucial to how the code functions.)

Re: Why asynchronous Rust doesn't work

#125
post #64

Earlier quoted context omitted.

Interesting! For comparison, Haskell went with the library approach but has the syntactic sugar of the equivalent of `and_then` built into the language. (I am talking about Monads and do-notation.) It's a bit like iterating in Python: for-loops are a convenient syntactic sugar to something that can be provided by a library.

Yes, a number of people have suggested introduction of a general do notation (or its analog) instead of usecase-specific async/awayt syntax, but since Rust does not have proper higher kinded types (and some Rust developers say it never will), such proposals have been deemed impractical.

It’s not just HKTs. Figuring out how to handle Fn, FnOnce, FnMut is a whole other can of worms.

Re: Why asynchronous Rust doesn't work

#126
post #107
post #13

Async isn't really the problem - the same issue pops up with error handling, with resource management, with anything where you want to pass functions around. The real problem is that Rust's ownership semantics and limited abstractions mean it doesn't really have first-class functions: there are three different function types and the language lacks the power to abstract over them, so you can't generally take an expres…

> mean it doesn't really have first-class functions: there are three different function types Rust absolutely does have first-class functions, though. Their type is `fn(T)->U`. The "three different function types" that you refer to are traits for closures. And note that closures with no state (lambdas) coerce into function types. higher_order(is_zero); higher_order(|n| n == 0); fn higher_order(f: fn(i32)->bool) { f(4…

If function literals don't have access to the usual idioms of the language - which, in the case of Rust, means state - then functions are not first-class.

> It's true that when you have a closure with state then Rust forces you to reason about the ownership of that state, but that's par for the course in Rust.

The problem isn't that you have to reason about the state ownership, it's that you can't abstract over it properly.

Re: Why asynchronous Rust doesn't work

#127
post #50

Earlier quoted context omitted.

It is in no danger of become as complex than C++. Nowhere near.

The pace it is going at, it’s not that far from it

I am curious where you get that impression. From what I could see, recent releases have just been "smoothing over pits" / filling in obvious type holes left over from older changes. Changes in rust edition 2021 are tiny and it seems to only exist in order to establish a regular cadence.

Re: Why asynchronous Rust doesn't work

#128
post #126
post #107

Earlier quoted context omitted.

> mean it doesn't really have first-class functions: there are three different function types Rust absolutely does have first-class functions, though. Their type is `fn(T)->U`. The "three different function types" that you refer to are traits for closures. And note that closures with no state (lambdas) coerce into function types. higher_order(is_zero); higher_order(|n| n == 0); fn higher_order(f: fn(i32)->bool) { f(4…

If function literals don't have access to the usual idioms of the language - which, in the case of Rust, means state - then functions are not first-class. > It's true that when you have a closure with state then Rust forces you to reason about the ownership of that state, but that's par for the course in Rust. The problem isn't that you have to reason about the state ownership, it's that you can't abstract over it pr…

You can't abstract over it in the same way that you can in Haskell, because you have to manage ownership. You can't abstract away ownership as easily, because the language is designed to make you care about ownership.

I write Rust code for my day job, and I frequently use map/reduce/filter. IMO if I can write all my collection-processing code using primitives like that, it's got first-class functions.

Re: Why asynchronous Rust doesn't work

#129
post #30

I've been primarily coding in rust since 2018. I never cared for async/await, and I've never used it. (at some point, coding event loops became very natural/comfortable for me, and I have no trouble writing "manual" epoll code with mio/mio_httpc). one nice thing about rust's async/await is, you don't have to use it, and if you don't, you don't pay for it in any way. sure, I run into crates that expect me to bring in…

I don't know how anybody can say this with a straight face.

Even in a systems context I think it's pretty reasonable to want to either perform or receive a HTTP request, as soon as you do that in Rust you are funneled into Hyper or something built on top of it (like reqwest) and instantly are dependent on tokio/mio.

The very first example in the reqwest readme^1 has tokio attributes, async functions AND trait objects. It's impossible that a beginner attempting to use the language to do anything related to networking won't be guided into the async nightmare before they have even come to grips with the borrow checker.

1. https://crates.io/crates/reqwest

Re: Why asynchronous Rust doesn't work

#130

I'm not totally sure what the author is asking for, apart from refcounting and heap allocations that happen behind your back. In my experience async Rust is heavily characterised by tasks (Futures) which own their data. They have to - when you spawn it, you're offloading ownership of its state to an executor that will keep it alive for some period of time that is out of the spawning code's control. That means all dat…

^this

IMO, Rust already provides a decent amount of way to simplify and skip things. Reference counting, async, proc_macro, etc.

In my experience, programming stuffs at a higher-level-language where things are heavily abstracted, like (cough) NodeJS, is easy and simple up to a certain point where I have to do a certain low-level things fast (e.g. file/byte patching) or do a system call which is not provided by the runtime API.

Often times I have to resort to making a native module or helper exe just to mitigate this. That feels like reinventing the wheel because the actual wheel I need is deep under an impenetrable layer of abstraction.

Post reply on HN