5 ms·
It's really not as bad as it's made out to be. You can paint yourself into a corner with it, but a lot of that is that async is fundamentally more complicated t
by vgel 4y ago
It's really not as bad as it's made out to be. You can paint yourself into a corner with it, but a lot of that is that async is fundamentally more complicated than sync / threaded code, and there's only so much any language can do to paper that over. Rust exposes a lot of details, so it can be complicated to get to grips with how they combine with async in certain corner cases, but the happy path is quite happy even now.
A lot of the async Rust code I work with already looks like `async fn foo() -> ... { do_request().await?.blah().await }`, plus the occasional gathering of futures into a `Vec` to join on. That sort of thing, not much different from Javascript, but with a lot more control of the low-level details.
A good deal of corner cases should get better once async traits are stabilized, which will mean much less need for manually writing out Future types. But honestly, even now it's not that bad. I have a codebase that uses async to read hundreds of thousands of files[1], streaming gunzip them, pass them to another future which streaming parses records from them, and then pushes those parsed records into a `FnMut` closure for further non-async processing. It took a bit of thinking and design to get everything moving together nicely, but that corner of the codebase now is only ~200 lines of pretty straightforward code -- there's like 1 instance of `Unpin`. It's not that bad.
[1]: I know async isn't necessarily faster for reading files, but it started life doing network requests and it can still saturate a 200-core machine so I haven't felt the need to port it over to threads.
- fasterthanlime 4y agoQuick aside: if you're willing to live the nightly life (unstable rustc), the `type_alias_impl_trait` feature gets you most of the way to "async trait methods". You still have to have a `Future` associated type, but in impl blocks, it just becomes `type Future = impl Future<Output = Blah>`, and then the compiler infers what the concrete (and probably unnameable, if you use async blocks) type is - no need to mess with `Pin<Box<T>>`. The most egregious code comes when implementing one of the `AsyncRead`/`AsyncWrite` traits or similar, and that can come up a bunch in backend services, for example if you want to record metrics on how/when/where data flows, apply some limits etc. I'm curious how the ecosystem will adapt once async trait methods land for real.
- inferiorhuman 4y agoFWIW I really don't like async in rust. It's improved significantly over the past couple years and it's nowhere near as bad as callback hell in Javascript but things still feel opaque. I've been toying around with a little monitoring agent (think Nagios or Sensu) to keep an eye on my defective LG fridge. So far I've managed to crash rustc twice. Trying to wrap my head around one library (that I was using incorrectly) I managed to "fork bomb" the damn thing and realize that I've little to no insight into the runtime. Try to find the current number of running tasks being managed by tokio… The beauty of the rust async stuff is that you can move to a multi-threaded runtime as you desire with minimal effort.
- fasterthanlime 4y ago> Try to find the current number of running tasks being managed by tokio… As a heavy user of async Rust in production (at a couple places), resource leaks / lack of visibility into that has been a top issue. In this area, tokio-console[1] is an exciting development. I have high hopes for it and adjacent tools in the future. (Instrumenting your app with tracing+opentelemetry stuff can help a lot, too). Until those become featureful/mainstream enough, Go has the upper hand in terms of "figuring out what's going on in an async program at any given time". [1]: https://lib.rs/crates/tokio-console https://lib.rs/crates/tokio-console
- inferiorhuman 4y agoOkay that's really cool, I'm going to have to play around with it.
- oxff 4y ago> The beauty of the rust async stuff is that you can move to a multi-threaded runtime as you desire with minimal effort. This is also a downside, having multi-thread be the default and not single-thread. It introduces some awkward / accidental trait bounds that are annoying to deal with if you want to do thread-per-core type of stuff IIRC.
- verdagon 4y agoI respectfully disagree; I don't think concurrency has to be that much more fundamentally complicated. It's likely that Rust's other design decisions are what made concurrency so difficult in Rust. Pony does fearless concurrency better IMO, and Forty2 shows how we can expand on Pony to be faster and more flexible. There are other approaches that have emerged recently too. For example, one can apply Loom's memory techniques to most memory management approaches to eliminate the coloring problem, to decouple functions from concurrency concerns. There are also languages which separate threads' memory from each other which allows them to do non-atomic refcounting, relying on copying for any messages crossing thread boundaries (though that's often optimized away, and could be even less than Rust's clone()ing elsewhere). One could also apply that technique to a language using generational references, if they want something without RC or tracing GC. Sometimes I wish Rust waited just a few more years before going all-in on async/await. Alas!
- vgel 4y agoI'd push back on "concurrency so difficult in Rust" -- because async isn't the only, or even best, way to do concurrency in Rust. I prefer using threads when I can, and Rust makes working with threads quite joyful[1]. I'd cautiously agree that it's possible async wasn't the best model to go "all in" on, though Rust is quite happily multi-paradigm so if something better comes along and has a notably different set of optimal use-cases than threads or async, I wouldn't be surprised to see Rust adopt it as well. I'm personally sort of skeptical about "color free async" because the models for sync/blocking IO and async IO are so different -- you can paper over the syntax differences, but you're going to be in a world of hurt when the semantic differences arise[2]. I'll admit I haven't tried a color-free async implementation myself though, so it's just speculation / sour grapes :-) > There are also languages which separate threads' memory from each other which allows them to do non-atomic refcounting, relying on copying for any messages crossing thread boundaries (though that's often optimized away, and could be even less than Rust's clone()ing elsewhere). Curious what you mean by this -- my understanding is that Rust also does this (i.e., you can `move |x|` a value into a thread and that thread owns it now, and then the thread can hand it back in a `JoinHandle`. That sort of sharing doesn't require an Arc or Mutex, since there's only one owner at a time. Is this something different? [1]: The other day I turned something reading in files from the filesystem sequentially into a custom threadpool passing blocks of parsed JSON over a MPSC channel that exposed the whole thing as a sequential iterator and it worked first try. I almost didn't believe it until I wrote the tests. [2]: E.g., "I wrote this and tested it with blocking IO but this syscall isn't supported by io_uring so in async mode it goes to a threadpool and passes some huge object in a message which kills perf with a huge memcpy", or some similar jank. Just spitballing on the type of thing I would fear happening, not a specific example.
- hgomersall 4y agoI really dislike threads and now I've grokked async (which, granted, took effort), I much prefer that world. I just find the design of my system is much cleaner and more robust than anything I've written in threads.