15 ms·
I really resonate with the async complaints in the article. Rust concurrency is not fearless. I fear writing async code every time I have to do it. I have to th
by dcow 4y ago
I really resonate with the async complaints in the article. Rust concurrency is not fearless. I fear writing async code every time I have to do it. I have to think so much harder which ultimately means opportunity for errors.
Even though Rust does slightly skirt the colored function issue, my code still bifurcates because truly correct async code needs to use async variants of the standard lib for file access. Knowing when something you call blocks, or grabs a lock, etc. is something which the compiler doesn’t help you with but should. And then how the hell are you supposed to use any code sanely when the entire community is using different async runtimes? Why can’t the runtime be part of the core language with hooks into parts that might need to be customized in hyper special cases?
It really feels like we lost the steam behind async Rust and we’ve got this partially complete thing that’s neither high level enough to be convenient nor low level enough to be fearlessly correct. Async traits still aren't a thing and it’s been years.
- MuffinFlavored 4y ago> I fear writing async code every time I have to do it. Devil's advocate: can you give an example of something complicated you ran into that wasn't "simply" solved by Arc<Mutex<T>>?
- dxuh 4y agoProbably any IO heavy workload, which usually requires async if it should be performant (like in all other programming languages too).
- dcow 4y agoEverything in my example. Scratching my head about whether I can call this part of my code from an async function because it might do filesystem operations or make a blocking network request. If I can’t, jumping through hoops to spawn an async task that can. Oh, now everything has to be static. Oh god async block syntax. I run into this a lot working on an API server written in Rust. It’s part of a larger project that has a CLI and lib crates. So I’ve run into everything I mentioned from poor language support for expressing async traits, to mixing and matching async runtimes and calling conventions.
- spoiler 4y ago> Scratching my head about whether I can call this part of my code from an async function because it might do filesystem operations or make a blocking network request. If I can’t, jumping through hoops to spawn an async task that can. If you have code that needs to wait on IO (regardless of how fast or slow it could be, the point is that it's not predictable), and want to call it from an async function, why not make that code an async function too? If it's library code, than yeah I agree it can suck. I tend to wrap those in my own API instead of inline spawning. At least it localises the uglyness, and that approach tends to compose well for me in practice. It does require a bit more code though, but my business logic code and plumbing code end up being separated and as a result easie to maintain too. > Oh, now everything has to be static. I will grant you that the static lifetime is a bit confusing until it clicks, but the issue is often that people have a wrong intuition about what a static lifetime bound means because it's introduced a bit clumsily and glossed over in the book. I agree that async blocks feel a bit "smelly" syntax wise (for lack of better word). I feel like some of these are just async woes that aren't trivial to solve, but I'd be surprised if the language teams weren't aware of this already and working towards solutions for these things. Async is very much MVP-ish in Rust anyway
- dcow 4y agoThe core of the problem from my angle is that rust’s stdlib is not async safe. It’s literally incorrect to call parts of the Rust stdlib from async code and the language tells you nothing about this. It doesn't even say “this function blocks are you sure you want to call it?”. So yeah I’ve got a bunch of my own library code (and others’) that I wrote normally and then later wanted to call it from async code and it’s a big mess. c.f. Swift’s async/await impl. Swift solved a lot of these woes because it ships a stdlib and runtime that supports async code.
- brundolf 4y ago> rust’s stdlib is not async safe. It’s literally incorrect to call parts of the Rust stdlib from async code and the language tells you nothing about this. Can you get more specific about what goes wrong when you call certain parts of the standard lib from async code? This is the first I'm hearing about this, and I'm not fully clear on what you mean by "not async safe"
- bjackman 4y agoThere were some popular comments on HN recently saying that manual threading has got much easier in Rust lately and it's now easier to just not use async. I have no idea about this myself, it's just a bit of public opinion that felt worth noting in the back of my mind!
- edflsafoiewq 4y agoWhat's changed to make it easier?
- vore 4y agoUnfortunately async is the only straightforward way you can do cancellation so there's still a lot of scenarios where you can't just use threading easily. I'm not sure why people are disagreeing with this: how would you cancel UdpSocket::recv without an async runtime? If you're using just the sync UdpSocket, you will need to go set a read timeout and repeatedly poll and insert your own cancellation points, or otherwise jury-rig your own mechanism up with syscalls to select/epoll/whatever, and be careful the whole time you're not leaking anything. However, if you're using an async runtime, you only have to drop the future and the recv is cancelled and the FD is dropped from the scheduler's waitset and everyone is happy.
- pas 4y agoIt depends on what the OS gives you. As far as I know the old BSD sockets-like Linux I/O syscalls are not really cancellable (the process would have to send a signal to itself to get interrupted, or close the fd, or pthread_kill ... https://stackoverflow.com/questions/17822025/how-to-cleanly-interrupt-a-thread-blocking-on-a-recv-call https://stackoverflow.com/questions/17822025/how-to-cleanly-... ) And that's usually where the zero-cost [compared to hand rolling it] abstractions come in, but it turns out that's not enough, we would also need composability (decomposability to be precise), because async Rust is doing a lot, especially if you need only the cancellation feature. Stepping back, it all depends on the development budget and goals. If time is short but there's plenty of RAM and performance budget, just go with Tokio. If that's too bloated, go with something low-level, eg. mio, and if you need every last bit of performance, then you are basically bargaining with the kernel anyway, so io_uring, and XDP (eBFP) ... and along the way there are potential stops at Seastar (a C++ framework) and dark alleys like DPDK (https://talawah.io/blog/linux-kernel-vs-dpdk-http-performance-showdown/ https://talawah.io/blog/linux-kernel-vs-dpdk-http-performanc...) :)
- jakear 4y ago> Even though Rust does slightly skirt the colored function issue [...] knowing when something you call blocks, or grabs a lock, etc. is something which the compiler doesn’t help you with but should. Are not colored functions precisely the compiler helping you identify when the code has the potential to block? If I write a block of JS in a non-async function context, I can be 100% guaranteed that no other task will interfere with my function's execution. If I write a block of JS in an async context, I can be 100% guaranteed that the only times when any other task may interfere with my functions execution are explicitly marked with `await`. I love this! I can't imagine writing serious code without it (barring some go-like messaging model), and I have no idea why people hate it so.
- dcow 4y agoI think you misunderstand my point slightly. Here’s the difference: in JS everything that needs to be async is actually marked async. So when you use a browser api or library function you know for certain. I agree I like that and want it for Rust. In Rust the standard lib is not async aware so it cant be used safely from async code, period. And tons of Rust code uses the stdlib and it’s not super practical to just not use it so you’re stuck asking whether something is safe to call from async code or not with zero help from the compiler. So we agree not having the compiler do this sucks. (What I meant by partially skirting the colored function issue is that you don't have to call await on the result of a function if you dint want to. So you can call a blue function from a red function just fine and do stuff with the result. You just can’t await it. This adds back lots of flexibility that some async impls in other languages don’t allow for.)
- jakear 4y ago> you can call a blue function from a red function just fine and do stuff with the result. You just can’t await it By this do you mean you can choose between waiting for a value in a way that halts the entire rest of the runtime while it resolves and one that will attempt to perform other tasks while waiting for the resolution?
- 4y ago
- brightball 4y agoOne thing that drew me to Rust was actually Elixir/Erlang calling out to it for certain specialized needs. Within Elixir/Erlang you get best of breed concurrency but exiting the BEAM to run other code is unsafe. Calling out to Rust, however, comes with great safety guarantees. Managing concurrency outside of Rust and then calling Rust for the more focused and specialized work is a good combination IMO. https://github.com/rusterlium/rustler https://github.com/rusterlium/rustler
- rapnie 4y agoNew to Rust, but found this project via Elixir forum that looks intriguing: https://lunatic.solutions https://lunatic.solutions
- codethief 4y agoThis does look intriguing! That being said: > Lunatic's design is all about super lightweight processes. Processes are fast to create, have a small memory footprint and a low scheduling overhead. They are designed for massive concurrency. > Lunatic processes are completely isolated, with a per-process stack and heap. If a process crashes, it won't affect others in the runtime. I don't understand. Is each Lunatic process an OS process or not? If it is, it sounds anything but lightweight. If it is not, it is not "completely isolated", at least not in the usual sense of the word.
- dcow 4y ago> If it is not, it is not "completely isolated", at least not in the usual sense of the word. Well that's where lunatic comes in. Lunatic provides the isolation and abstraction so you can run various wasm binaries side by side. If one of those binaries segfaults, lunatic presumably catches the interrupt and kills the corresponding wasm instance that caused it rather than just giving up and crashing.
- codethief 4y agoI was rather referring to security boundaries – if lunatic processes run inside the same OS-level process, the sandboxing cannot, almost by definition, be as good.
- duped 4y ago> And then how the hell are you supposed to use any code sanely when the entire community is using different async runtimes It's not that big of a deal in practice. You pick a runtime. Most of the library ecosystem doesn't care about the async runtime used, and that which does uses tokio. > Why can’t the runtime be part of the core language with hooks into parts that might need to be customized in hyper special cases? Because Rust ships a minimal runtime. > Async traits still aren't a thing and it’s been years. They were blocked on generic associated types which landed recently, and will land in stable in the near future. For everything else there's `#[async_trait]`. Also, "fearless concurrency" is a lower level kind of "fearless" than "how do I use an async API." It's about memory safety and program soundness in the presence of concurrent execution. The fact that the only fears we have are about the ecosystem and APIs is a testament to how Rust eliminated the deep fear of concurrent execution. And if we're going to take a shot at async, let's take a shot at how fucking hard it is to implement a correct async executor and how bad the docs are. std Rust does next to nothing to abstract or make your life easier.
- dcow 4y agoI use Rust in practice. We use async_std. Someone recently added reqwest. We now have two async runtimes. It’s a problem. “minimal runtime” is semantics. Rust shipped async. IMO that would expand the definition of what is minimal for Rust to include in the runtime. I’m 100% certain no async anything would need to make it into programs that don't use async so the surface area of the runtime need not change for anybody unduly if Rust supported async. I’m not saying Rust hasn't made async things better. It has. But it’s still really messy. It doesn't give me the same peace of mind that Rust does around memory safety. And that’s my gripe. I admit it’s a rather privileged one. But that’s Rust’s fault. Rust has shown me how nice it is to fight with the complier till I want to scream if it means my program isn't going to have dangling pointers everywhere. I am a lazy dev and want the same for my concurrent code. Rust currently doesn't deliver on that promise, IMO. And it surely could with some continued work and slight philosophy adjustment. It’s literally incorrect to use parts of Rust’s stdlib from async code. You can’t tell me that’s a good thing. C.f. Swift where the async support is thoroughly cooked into the language, runtime, and stdlib. Rust can do better here.
- tipsytoad 4y agoIt's coming! https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-in-trait-nightly.html https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-i... Although I'm still not sure how they return the unsized Future without the whole pin box method
- SpaghettiCthulu 4y agoMy understanding is that `async fn` in a trait is desugared to `fn() -> impl Future` just like it would be anywhere else, so there's no unsized types involved.
- pitaj 4y agoIt's actually desugared to a generic associated type: trait Foo { type bar_Returns<'s>: Future<Output = Result<u64, ()>> where Self: 's; fn bar<'s>(&'s self) -> Self::bar_Returns<'s>; }
- necubi 4y agoIt took me about of month of writing async rust (coming from a solid sync rust background) to feel comfortable. In that first month there were definitely a lot of inscrutable issues (fortunately the tokio discord was very helpful). But now it feels productive, and if you're totally in the async world, it all works pretty well. The lack of async traits is my main pain point today, but you can mostly work around that with the async_trait crate. Generally it's been slow progress on usability but I think it's in a much better place than it was a year ago, and will clearly be better in another year.
- adql 4y agoI really feel that's this is just inherent to async/await model, you can only put so much lipstick on a pig. Light threads + messaging (whether proper like Erlang or bastardized like Go) feels like far superior model for concurrency in near-all cases and also easier to write and reason about (series of "functions" sending messages looks very similar to serial code) but I'd imagine it would be very hard to make convenient in GC-less language
- Verdex 4y ago> I really feel that's this is just inherent to async/await model, you can only put so much lipstick on a pig. I've been coming to the same conclusion after working with C# async/await significantly in the last few years. I suspect that async/await in javascript was better than callbacks, so it entered the industry zeitgeist as absolutely good as opposed to relatively good.
- someusername123 4y agoWe actively ban Rust's async /await syntax from our codebase for a few reasons, though primarily it's been an issue with it's lack of maturity. If feels incredibly painful to use in all but the most trivial cases. However; even if were to be much more user friendly, it still would be the wrong model for most of our code since we care significantly more about parallelism then about concurrency. Having use async/await in production a bit, I wouldn't really recommend it to anybody: the async runtimes are large dependencies typically that are difficult to roll your own, understand or debug. Code built on top of them has numerous performance issues that I uncovered when running it through it's paces. Much as I'm a fan of Rust, a fan of async await, I am not.
- dcow 4y agoAre there any web server frameworks built on the now standardized mspc concurrency model?
- kouteiheika 4y ago> And then how the hell are you supposed to use any code sanely when the entire community is using different async runtimes? Honestly, and this might be a hot take, but I think that's easy to solve. Just pretend that tokio is in the standard library and use it. And refuse to support anything else. Tokio is stable, fast, well maintained and supported. There's really no reason to use anything else in 98% of cases.
- dcow 4y agoThe problem is that this.. preference.. isn't made clear or justified anywhere. I use `async_std` because its value prop is "the stdlib but for async". I don't want some weird thing called `tokio` in my codebase with extra bells and whistles I don't need. async-std is a minimal simple fast async runtime for Rust. I am aligned with their vision and love using it. async-std works flawlessly and only really lacks more comprehensive stream support which I augment with the futures crate as is pretty standard. If yall want everyone to use Tokio then tell me why it's better than async_std. Tell me why it's the de-facto community standard. Because "popular web framework" uses it? Why are all the cool kids using it? Because the others are? No. Either I want some group with authority to bless a core async runtime implementation that aligns with Rust's philosophy and value prop and bring it under their wing so that it can be made to serve the goals of the project which might include being configurable enough to deploy in all of the myriad scenarios and environments where people use Rust today. Or in lieu of that, provide the necessary abstraction layer so everyone can bring their own executor like it's supposed be. The entire premise of Rust not providing a blessed implementation is so that people could pick and choose based on their preferences and use cases. That's the argument. The reality is that the goal is not realized because Rust has not done the work to enable everyone to simply bring their own runtime and call it day. So Rust has not delivered on the core reason for not shipping a runtime in the first place. The solution of "just use a de-facto blessed runtime" really seems like a concession if Rust's argument for not having one is to be respected.
- hdevalence 4y ago> If yall want everyone to use Tokio then tell me why … it's the de-facto community standard. > I want some group with authority to bless a core async runtime implementation… Hmm, if Tokio is the de-facto community standard, isn’t that saying that the authority of “people who write and run async code for actual production systems” is blessing it as a preferred option? > The reality is that the goal is not realized because Rust has not done the work to enable everyone to simply bring their own runtime Or, another possibility is that it hasn’t been realized because building a robust, production-ready, well-maintained runtime is itself a lot of work, and Tokio is the only project that has actually done it.
- puffoflogic 4y agoRust's "fearless concurrency" always referred to traditional thread-based concurrency, which works very well for 99% of usecases. Async is something that was bolted on to the language long afterwards by folks coming from the JS/Node community who didn't understand threads or the value of fearless concurrency.
- jbritton 4y agoThere is something I don’t understand. socket recv() is blocking by default. If blocked in recv() there is no safe way to exit the thread. One can set a timeout, but I can’t imagine this works well with lots of threads with a recv() on each thread. One can use select() which has mostly the same problems, either block potentially forever or use a timeout. Although one can create an additional 2 sockets, connect them together and put one of them in the select read set to allow controlled unblocking, but this doesn’t feel like a great solution, so I have always used async for socket code, because I just don’t know how it’s possible to use sync code. I have read you can call close(fd) and get recv(fd) to return, but I also hear this is not safe.