Hacker News new | past | comments | ask | show | jobs | submit login

> But keeping the current half-baked async implementation doesn't make the language smaller or simpler. It just makes the language worse.

I can't disagree more.

In fact, I think that the current state of async Rust is the best implementation of async in any language.

To get Pin stuff out of the way: it is indeed more complicated than it could be (because reverse compatibility etc), but when was the last time you needed to write a poll implementation manually? Between runtime (tokio/embassy) and utility crates, there is very little need to write raw futures. Combinators, task, and channels are more than enough for the overwhelming majority of problems, and even in their current state they give us more power than Python or JS ecosystems.

But then there's everything else.

Async Rust is correct and well-defined. The way cancellation, concurrent awaiting, and exceptions work in languages like JS and Python is incredibly messy (eg [1]) and there are very few people who even think about that. Rust in its typical fashion frontloads this complexity, which leads to more people thinking and talking about it, but that's a good thing.

Async Rust is clearly separated from sync Rust (probably an extension of the previous point). This is good because it lets us reason about IO and write code that won't be preempted in an observable way, unlike with Go or Erlang. For example, having a sync function we can stuff things into thread locals and be sure that they won't leak into another future.

Async Rust has already enabled incredibly performant systems. Cloudflare's Pingora runs on Tokio, processing a large fraction of internet traffic while being much safer and better defined than nginx-style async. Same abstractions work in Datadog's glommio, a completely different runtime architecture.

Async Rust made Embassy possible, a genuine breakthrough in embedded programming. Zero overhead, safe, predictable async on microcontrollers is something that was almost impossible before and was solved with much heavier and more complex RTOSes.

"Async Rust bad" feels like a meme at this point, a meme with not much behind it. Async Rust is already incredibly powerful and well-designed.

[1]: https://neopythonic.blogspot.com/2022/10/reasoning-about-asy...






> In fact, I think that the current state of async Rust is the best implementation of async in any language.

Hahahaha hard disagree. Last year I implemented the braid protocol (a custom streaming protocol using HTTP) in javascript in less than an hour and about 30 lines of code. Then I spent 2 weeks trying to do the same thing in rust - writing hundreds of lines of code in the process and I couldn't get it to work. Eventually I gave up.

I got it working recently - but only by borrowing some wild tricks from reading the source code of tokio, that I never would have thought of on my own.

> To get Pin stuff out of the way: it is indeed more complicated than it could be (because reverse compatibility etc), but when was the last time you needed to write a poll implementation manually?

Last week, while writing a simple networked database application. Again I needed to produce an async stream, and thats impossible using async fn.


> Then I spent 2 weeks trying to do the same thing in rust… Eventually I gave up.

In my experience, that kind of difference boils down to a combination of three things.

- Comparing apples and oranges. For example, Box makes pinning trivial (you can just move in and out of Pin no problem), but oftentimes people new to Rust try to prematurely optimise and eliminate a single pointer lookup. If that's the case, were you really writing the same thing in JS and in Rust?

- An extension to the previous point, the behaviour is usually different. What would happen in your JS implementation if two streams were awaited concurrently, one received a message, and the other had to be cancelled? What if one threw an exception? In Rust, you're forced to think about those things from the start. In JS, you're coding the happy path.

- Trying to reproduce the exact same architecture even if it's awkward of inefficient. For example, it's really really easy to use a stream wrapper [1] to produce a stream from a channel, but then the architecture gets very different.

> Again I needed to produce an async stream, and thats impossible using async fn

I strongly recommend a channel instead. There's also async_stream [2], but channels are simpler and cleaner.

Over two years of writing embedded, web, and CLI rust I didn't have to write a raw future once.

[1] https://docs.rs/tokio-stream/latest/tokio_stream/wrappers/in...

[2] https://docs.rs/async-stream/latest/async_stream/


> To get Pin stuff out of the way: it is indeed more complicated than it could be (because reverse compatibility etc), but when was the last time you needed to write a poll implementation manually?

Often. Pin and Poll contribute to the problem of having a two-tiered ecosystem: people who can use async and people who can contribute to async internals. That's a problem I'd love to see fixed.

This is one of the reasons we've spent such a long time working on things like async-function-in-trait (AFIT), so that traits like AsyncRead/AsyncBufRead/AsyncWrite/etc can use that rather than needing Pin/Poll. (And if you need to bridge to things using Poll, it's always possible to use Poll inside an async fn; see things like https://doc.rust-lang.org/std/future/fn.poll_fn.html .)


I agree wholeheartedly (and I'm not surprised that you of all people often write raw futures!). I want to push back on the "async rust bad/failure/not ready" meme because

- it's perfectly possible to be a successful user of the async ecosystem as it is now while building great software;

- this two-tiered phenomenon is not unique to Rust, JS and Python struggle with it just as much (if not more due to less refined and messier design). As an example, [1] is elegant, but complex, and I'm less sure it's correct compared to a gnarly async Rust future, because the underlying async semantics are in flux.

Of course I'd love for the remaining snags (like AFIT) to go away, and simplified Pin story or better APIs would be great, but this negativity around async Rust is just wrong. It's a massive success already and should be celebrated.

[1]: https://github.com/florimondmanca/aiometer/blob/master/src/a...


> I want to push back on the "async rust bad/failure/not ready" meme because

Absolutely; to be clear, I think async Rust has been a massive success, and has a lot of painfully rough edges. The rough edges don't invalidate the massive success, and the massive success doesn't invalidate the painfully rough edges.


> - Comparing apples and oranges. For example, Box makes pinning trivial (you can just move in and out of Pin no problem), but oftentimes people new to Rust try to prematurely optimise and eliminate a single pointer lookup. If that's the case, were you really writing the same thing in JS and in Rust?

Pointer lookups are cheap-ish, but allocating can be extremely expensive if you do it everywhere. I've seen plenty of lazy, allocation & clone heavy rust code end up running much slower than the equivalent javascript. I assume for this reason.

But in this case, I couldn't get it working even when putting Box<> all over the place.

> What would happen in your JS implementation if two streams were awaited concurrently, one received a message, and the other had to be cancelled? What if one threw an exception? In Rust, you're forced to think about those things from the start. In JS, you're coding the happy path.

I implemented error handling in the javascript code. That was easy - since async generators in javascript support try-catch. Javascript doesn't support concurrent execution - so that problem doesn't exist there.

Did multithreading contribute to javascript being easier to write than rust? Who cares? I had a problem to solve, and javascript made that trivial. Rust made it a total nightmare.

I didn't know about the stream wrappers when I started coding this up. That was how I eventually found an the answer to this problem: I read that code then adapted their approach.

And by the way, have you read the code in those wrappers? Its wild how they glue manual Future implementations and async functions together (with some clever Boxes) to make it work. It blew my mind how complex this code needs to be in order for it to work at all.

> Over two years of writing embedded, web, and CLI rust I didn't have to write a raw future once.

I'm happy for you, and I wish I had the same experience. Streams are bread and butter for my work (CRDTs, distributed systems and collaborative editing). And at this rate? Proper support for streams in rust is probably a decade away.


Every single JS future is boxed. Moreover, they aren't just boxed, they are often backed by a hashmap (which may or may not be optimised away by the JIT). Elaborate allocation-free async is not an apple-to-apples comparison.

JS does support concurrent execution, Promise.all is an example. Without it, JS async would make little sense. The problem very much exists there, and try-catch is only a surface-level answer. As you can see here [1], the interaction of cancellation and async in JS is at least just as (or more) complex than in Rust.

By the way, multithreading has little to do with Pin. I presume you're thinking of Send bounds.

"To work at all" is very dismissive. Those wrappers are complex, but very well abstracted, well defined, and robust, the complexity is essential. Again, look at [1], JS async is hardly less complex, but also much more vague and ill-defined.

[1]: https://github.com/whatwg/streams/issues/1255


Honestly I’m not really sure what you’re arguing here. Are you agreeing or disagreeing that solving this problem in rust is currently significantly more complex than solving it in JavaScript? I already told you I implemented error handling just fine in JavaScript. Do you think I’m lying? Do you want to see the code, so you can grade it?

The apples-to-apples comparison I’m making here is: “I sit down at my computer with the goal of solving this problem using code. How long before I have a robust solution using the tool at hand?”. Of course the internals of rust and JavaScript’s Future/promise implementations are different. And the resulting performance will be different. That’s what makes the comparison interesting.

It’s like - you could say it’s an apples to oranges comparison to compare walking and driving. They’re so different! But if I want to visit my mum tomorrow, I’m going to take all those variables into account and decide. One of those choices will be strictly better for my use case.

Rust came off terribly in the comparison I made here. I love rust to bits in other ways, but dealing with async streams in rust is currently extremely difficult. Even the core maintainers agree that this part of the language is unfinished.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: