Live data from Hacker News

Why asynchronous Rust doesn't work

theta.eu.org

151–160 of 499 posts

Re: Why asynchronous Rust doesn't work

#151
post #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…

Try https://github.com/algesten/ureq, it’s very nice and not async.

Re: Why asynchronous Rust doesn't work

#152
post #133

Earlier quoted context omitted.

>That is not an accurate summary of that comment. How is to so, if he explicitly writes: > Suggestions that we should revisit our underlying futures model are suggestions that we should revert back to the state we were in 3 or 4 years ago, and start over from that point. Trying to provide answers to these questions would be off-topic for this thread; the point is that answering them, and proving the answers correct,…

> How is to so, if he explicitly writes: There's a difference between "we decided this 3 years ago" and "we rushed the decision". At this point, it's no longer possible to weigh the two models on a neutral scale, because changing the model would cause a huge amount of ecosystem churn. But that doesn't mean they weren't properly weighed in the first place. Regarding cyclicity… well, consider something like a task runn…

>There's a difference between "we decided this 3 years ago" and "we rushed the decision".

As far as I understand the situation, the completion-based API simply was not on the table 3 years ago. io-uring was not a thing and there was a negligible interest in properly supporting IOCP. So when a viable alternative has appeared right before stabilization of the developed epoll-centric API, the 3 year old decision has not been properly reviewed in the light of the changed environment and instead the team has pushed forward with the stabilization.

>because changing the model would cause a huge amount of ecosystem churn.

No, the discussion has happened before the stabilization (it's literally in the stabilization issue). Most of the ecosystem at the time was on futures 0.2.

Regarding your examples, I think you simply look at the problem from a wrong angle. In my opinion compiler should not desugar async fns into usual functions, instead it should construct explicit FSMs out of them. So no need for Arcs, the String would be stored directly in the "output" FSM state generated for foo. Yes, this approach is harder for compiler, but it opens some optimization capabilities, e.g. regarding the trade-off between FSM "stack" size and and number of copies which state transition functions have to do. AFAIK right now Rust uses "dumb" enums, which can be quite sub-optimal, i.e. they always minimize the "stack" size at the expense of additional data copies and they do not reorder fields in the enum variants to minimize copies.

In your example with two sub-tasks a generated FSM could look like this (each item is a transition function):

1) initialization [0 -> init_state]: create requests A and B

2) request A is complete [init_state -> state_a]: if request B is complete do nothing, else mark that request A is complete and request cancellation of task B, but do not change layout of a buffer used by request B.

3) cancellation of B is complete [state_a -> state_c]: process data from A, perform data processing common for branches A and B, create request C. It's safe to overwrite memory behind buffer B in this handler.

4) request B is complete [init_state -> state_b]: if request A is complete do nothing, else mark that request B is complete and request cancellation of task A, but do not change layout of a buffer used by request A.

5) cancellation of A is complete [state_b -> state_c]: process data from A, perform data processing common for branches A and B, create request C. It's safe to overwrite memory behind buffer A in this handler.

(This FSM assumes that it's legal to request cancellation of a completed task)

Note that handlers 2 and 4 can not be called at the same time, since they are bound to the same ring and thus executed on the same thread. Other completion handler simply can not call another handler, since they are part of the same FSM and only one FSM transition function can be executed at a time. At the first glance all those states and transitions look like an unnecessary complexity, but I think that it's how a proper select should work under the hood.

Re: Why asynchronous Rust doesn't work

#153
post #125

Earlier quoted context omitted.

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.

I think there were some (related, I think!) proposals here: https://github.com/rust-lang/lang-team/issues/23 https://github.com/rust-lang/rfcs/issues/3000

Re: Why asynchronous Rust doesn't work

#154

> 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 abo…

With standard library support, green threads can be just fine. I've worked with many, many C programs that were nbio and green threads. Cancellation is trivial when you are writing the scheduler! In these cases, I used multiprocessing when I needed parallelism. The answer to "If you're blocking on a socket recv in a thread..." is to not ever do that; the library should provide a green recv that appears to be blocking to the code, but is implemented with non-blocking calls underneath. With the privilege of being the standard library, there are many ways to enforce this.

N to M is much harder to get cancellation right, but again if you write the scheduler, you can define the cancellation points, so you are not beholden to undefined behavior. Instantly cancelling a thread is just not solvable, and for Rust, which cares about low-level performance &c. then long-running computations could explicitly check for cancellation when it is needed. Threads that perform I/O regularly will naturally hit cancellation points.

Cancellation is a huge problem in concurrency, and the usual answer to "how do I cancel a thread" is "you don't want to cancel a thread" which is rather unsatisfying. As you point out, it's not like using async and/or promises automatically solves the problem either; the only way to get it right is to build it in from the start.

Re: Why asynchronous Rust doesn't work

#155
post #86

The big issue is that non-GC concurrency is still effectively a CS research topic. In a GC language, you can "just" let the GC system clean up after you're done--who owns allocation and cleanup isn't an issue. In a non-GC language, who allocates and cleans up what and who pays for that suddenly becomes a fundamental part of the domain. For example, I have seen a zillion "lock-free" data structure libraries in Rust, a…

GC languages aren't significantly better, because the GC usually only directly manages the memory[1], and there are many, many other resources that need to be managed correctly.

1: Many GC languages let you run arbitrary code when the associated memory is freed, but if you have to wait for a full GC pass to happen before e.g. a lock is released or a file descriptor is flushed, that will cause many issues as well.

Re: Why asynchronous Rust doesn't work

#156

> Was spinning up a bunch of OS threads not an acceptable solution for the majority of situations? It's worth remembering that this is still a very acceptable thing to do in some cases. And spawning OS threads is sometimes easier to write and easier to read, and easier to reason about than async code. Just because async is in the language, don't feel like you need to use it everywhere.

> Just because async is in the language, don't feel like you need to use it everywhere.

Except that you will have to because no alternative library exist

Re: Why asynchronous Rust doesn't work

#157
post #136
post #85

Earlier quoted context omitted.

It's a problem in that you can't arbitrarily compose synchronous and asynchronous functions. Many languages have a distinction between statements and expressions, and some language designers thought that was ugly and designed languages where everything is an expression. But because of how people think, an application typically has a rough hierarchy to it by the designer's intent. So the fact that this becomes a bit m…

You can't arbitrarily compose 2 functions with mismatched types. So i don't think this argument holds in a typed language.

The problems are

(1) either you write two versions of almost every function or you can only support one use case (sync or async). If you could write generic functions which work in both cases it would be better.

(2) it creates a lot of noise writing async/await. It's like if we had to always write x = call f() in the sync case.

Re: Why asynchronous Rust doesn't work

#158

Earlier quoted context omitted.

>Just because one disagrees with the conclusion does not mean that the conclusion was made in haste or in ignorance. Believe me, I do understand the motivation behind the decision to push async stabilization in the developed form (at least I think I do). And I do not intend to argue in a bad faith. My point is that in my opinion the Rust team has chosen to get the mid-term boost of Rust popularity at the expense of t…

> My point is that in my opinion the Rust team has chosen to get the mid-term boost of Rust popularity at the expense of the long-term Rust health. I don't think a conscious decision of that sort was made? My impression is that at the time the road taken was understood to be the correct solution and not a compromise. Is that wrong?

The decision was made 3 years ago and at the time it was indeed a good one, but the situation has changed and the old decision was not (in my opinion) properly reviewed. See this comment: https://news.ycombinator.com/item?id=26408524

Re: Why asynchronous Rust doesn't work

#159
post #146

Earlier quoted context omitted.

I mean, you can always just use an async crate and use async_std::task::block_on.

Only if no other crate in your executable uses tokio or another executor, right? Mixing executors is approximately fatal, AFAICT.

I think that's too strong.

You could have two executors with no problem.

What might cause issues is if code running on one executor needed close interaction with the other - such as running a task on the other executor.

Most of the time this is just avoided by using only one executor. But that's not the only way.

Re: Why asynchronous Rust doesn't work

#160
post #57

I like to joke that the best way to encounter the ugliest parts of Rust is to implement an HTTP router. Hours and days of boxing and pinning, Futures transformations, no async fn in traits, closures not being real first class citizens, T: Send + Sync + 'static, etc. I call this The Dispatch Tax. Because any time you want more flexibility than the preferred static dispatch via generics can give you - oh, so you just w…

In a systems context, where performance and memory ostensibly matter, why wouldn’t you want to be made aware of those inefficiencies? Sure, Go hides all that, but as a result it’s also possible to have memory leaks and spend extra time/memory on dynamic dispatch without being (fully) aware of it.

In this example rust doesn't just make me aware of the tradeoffs. It almost feels like the language is actively standing in the way of making the trade offs I want to make. At least as the language is today. I think a bunch of upcoming features like unsized rvalues and async fns in traits will help.
Post reply on HN