14 ms·
Rust does not have to be this hard. Most of the pain here come from the unholy trifactor: combining async, lifetimes and dynamic dispatch with trait object clo
by the_duke 4y ago
Rust does not have to be this hard.
Most of the pain here come from the unholy trifactor: combining async, lifetimes and dynamic dispatch with trait object closures; which is indeed very awkward in practice.
Async support is incredibly half-baked. It was released as an MVP, but that MVP has not improved notably in almost three years.
There are lots of ideas, some progress on the fundamental language features required (GAT, existential types, ...) and the random RFC here and there, but progress is painfully slow.
This isn't because no one cares, but because Rusts async implementation (which is very cool due to the low overhead) and its interactions with the other language features require complicated extensions to the type system. It does seem to me like there might be a lack of resources/coordination/vision ever since the Mozilla layoffs, but that's a different topic.
If you can avoid async I would recommend doing so. The problem is that the entire ecosystem has completely shifted to async. There are almost no active / popular libraries related to network IO that haven't switched over.
To be clear: using async is fine if you know what you are doing, and it can provide incredible performance. But if you do, keep it simple: avoid lifetimes and most importantly: don't attempt advanced trait shenanigans - if you do need traits, just returned BoxFutures without lifetimes, throw in lots of clone(), share as little data as possible, use Arc<tokio::sync::Mutex<_>>, and call it a day.
- Animats 4y ago> If you can avoid async I would recommend doing so. The problem is that the entire ecosystem has completely shifted to async. There are almost no active / popular libraries related to network IO that haven't switched over. Yes. I've been complaining about async contamination for some time. I'm writing heavily threaded code, with threads running at different priorities, and libraries which want async get in the way. If you look at the poster's example, the "Arc" version is very close to the Go version. And if it didn't use "async", it would be even closer. Go's green threads simplify things. It's real concurrency; you can block. But there are times when you have to lock. As I've said before, if you're writing webcrap, use Go. The libraries for web-related stuff are stable and well-exercised, since Google uses them internally.
- cmrdporcupine 4y agoI agree, I don't understand why so many people lately seem to want to use Rust for web domain stuff. I don't like Go, I hated the year I had to work in it @ Google. But frankly, it's better suited for 'server' type stuff, unless you're talking about a very specific type of server that has super intense latency guarantees. And now that Go has generics, I'd probably hate it less. Go is the new Java. Rust is the new C++. Let's just stick with that.
- crabbygrabby 4y agoWhy? Because it's stupid fast, fast means serving an order of magnitude or more clients before requiring scale up. Scale up means $. No stop the world GC time situations, etc. That's basically it. To be fair, I usually prefer to use go as well, lately though rust is more appealing.
- cmrdporcupine 4y agoI mean, there's no world in which my personal tastes wouldn't prefer Rust over Go -- I quite dislike writing Go. But as an engineer it is my responsibility to use the right tool for the job, not pick tools based on my tastes or gut feelings. There's different kinds of fast. For most kinds of fast that people doing 'web' type things need, a garbage collector and a VM are not going to be the bottleneck. Efficient management of workloads across blocking I/O is going to be where the hard work is. Now, I've written ad servers and video streamers, and other low latency high throughput things, and yes, I'd probably reach for C++ or Rust there. But some of the jobs I've seen lately posting for Rust, I do question. Even if I'm tempted to apply, because I'd like to get $$ to work in Rust.
- crabbygrabby 4y agoYea it's hard to say without context, I just gave the generic answer. I'm not sure you ever 'need' a garbage collector, and if avoiding one lets me save 20k in cloud expenses a year on a small team, it's probably worth it, because after scale out 20k isn't 20k anymore. That said, people misuse technology all the dang time(I've done it). And in general I agree, Go is usually enough and has the best cloud ecosystem. I've had trouble with large go code bases exploding over time and requiring a lot of bodies to maintain compared with rust/scala. Sometimes it's hard to speculate when one tool is clearly better than another. Some companies just want to use new stuff to sound cooler...
- PragmaticPulp 4y ago> To be clear: using async is fine if you know what you are doing, and it can provide incredible performance. But if you do, keep it simple: avoid lifetimes and most importantly: don't attempt advanced trait shenanigans - if you do need traits, just returned BoxFutures without lifetimes, throw in lots of Arc<Mutex<_>>, clone() and call it a day. It seems like everyone doing async Rust goes through a long journey before arriving at this conclusion. Once you know the pitfalls you can navigate around them, but I hit a lot of dead ends along the way.
- lumost 4y agoAnecdotally, this is the pain felt most across the entire rust language. There are more ways not to do something than to do something. This makes it hard for beginners to pickup, and difficult for projects to scale on.
- crabbygrabby 4y agoI've heard people complain about languages having too many ways to do one thing. Never have I heard the opposite complaint hahaha. You can't please everyone.
- yencabulator 4y agoThat by itself can be interpreted as a good thing -- no Perl TMTOWTDI please. The "special quality" about Rust that hurts here is that it takes a lot of time going into a dead end before you realize it's a dead end.
- ericbarrett 4y agoCompletely agree. I wrote a multithreaded compute-heavy program in Rust, semi-ported from a C++ version, and sidestepped async completely—just used old school thread primitives. It was delightful! Once I sorted out the data structures and messaging, the borrow checker and Send/Sync traits made implementation nearly trivial, and absolutely no memory corruption or accidental non-atomic clobbering. It took a bunch of hours with gdb and valgrind to achieve the same stability with the C++ code, and to this day I'm not 100% sure I got every edge case.
- Waterluvian 4y agoI’m a Rust newbie. Mind if I ask: are you referring to using threads and locks and queues and such? Does Rust give you rope to hang yourself when doing it without async or does it continue to be very specific about forcing you to guarantee that you’re not going to run into races and whatnot?
- jacksnipe 4y agoOh my god we’ve strayed so far from the light T_T Once upon a time, the whole promise of Rust was “fearless concurrency”.
- infogulch 4y agoI don't understand. What about this thread would make you fear running rust? Fear of learning a solution to rusts memory model, and 'fighting with the borrow checker' as they say -- sure. But rust never claimed that you wouldn't have to climb a steep learning curve, it claims that if you do make it up then the result can't hurt you due to a memory fault. So "fallen from the light" seems a little overdramatic.
- charlieflowers 4y agoIMO fallen from the light sounds spot on (not gp). I’m shocked to hear the state of async from real practitioners saying to avoid it.
- ipodopt 4y agoIt has been a sec but if I were to do another multi-threaded async Rust project I would do one thread per async runtime and explicitly pass anything that needed to be shared. This should be more ergonomic as this should get rid of everything needing to have send/sync traits. I also suspect it may be more performant as I am not sure how good the async runtimes are about keeping scopes pinned to a particular core so its not constantly jumping around and busting the l1 caches (which would be extremely detrimental to compute latency and bandwidth)... Happy to be schooled on any of this.
- bombela 4y agoBut what when you have some threads slacking off, and others too busy? It would be nice in this case to use those idle threads, even if it means a little bit of CPU cache trashing. And I believe this is what Tokio offers with a work stealing thread pool.
- ipodopt 4y agoTrue, but I suspect that without a truly global prescient scheduler it is almost never worth it to core switch unless you generally have really long tasks. For an efficient core context switch the scheduler must accurately predict that the source (current) core won't be free for the duration of the full core context switch time and that the sink core will be free by the time the meta context gets there and will have been free by the time the rest gets there. Otherwise, the scheduler ends up thrashing the cpu (it is actually a bit worse as future task might need same context so you have to be aware of the future). So, for the scheduler to know this it would need to be: - Global: The only scheduler on the system or basically rafting with all the other schedulers on the system - Prescient: The scheduler(s) would need to be able to predict all tasks, thier context, and work time per task perfectly. Which could really could only happen when everything is static and hence deterministic. For example, I think most tasks people are throwing at async are web requests. Most actually take the core an order of magnitude shorter time to compute then the time it takes passing the context from one core to another and they are all unpredictable to the scheduler. In this scenario I could see the scheduler taking up the majority of computational time on the system. So turn on multi-threading + async on a quad core and you will get worse bandwidth and latency(always) for all your pains. EDIT: Although this single data point would tell me I am wrong (see description): https://www.youtube.com/watch?v=IG-wGXENTt8 https://www.youtube.com/watch?v=IG-wGXENTt8
- epage 4y ago> This isn't because no one cares, but because Rusts async implementation (which is very cool due to it's low overhead) and it's interactions with the other language features require complicated extensions to the type system. It does seem to me like there might be a lack of resources/coordination/vision ever since the Mozilla layoffs, but that's a different topic. I wonder how much technical debt is slowing things down. It seems like we keep hitting a breaking point where we'll finally be forced onto polonius and chalk but people keep finding ways to extend the existing implementation to make things work, kicking the can down the road.
- dhosek 4y agoI might be (probably am) missing something, but for the use case described, it seemed odd to me to have borrowing come into play at all. I would think that rather than a borrowed reference with a lifetime, the dispatcher could just consume the object coming in. The term “moving” is a bit misleading in that it carries with it an implied heavyweight operation that likely doesn't come into play at all.
- azemetre 4y agoIn your experience what languages would you say handle async well? Genuinely curious. I’ve only ever done JS professionally for a decade but started branching out into python, rust, and kotlin due to personal projects.
- javajosh 4y agoInterestingly, I've become a big fan of node's single-threaded "one big loop" model, which means multitasking is cooperative instead of preemptive. This strikes me as more honest, somehow. It doesn't distract you with abstractions (like threads) that don't make sense in this context. Most production workloads these days will be a docker process assigned to (at best) a single sticky core/thread on a blade somewhere - so in terms of resources, node is quite honest about what you actually have to work with. (This as opposed to, say, a Java process, which wants to believe it has control over an entire physical server CPU, and when you run it in a docker process, you're just exercising virtualization overhead if you use Thread, etc.) That said, if you have a physical server, Java is quite good, especially with the upcoming Project Loom improvements. The langspec and vmspec have gotten fat the last 20 years, but at least those documents exist. Plus there is OpenJDK which is a great enabler and calmer of nerves; there's a reason so many alt langs target the JVM, and they are good. Groovy, Clojure, Kotlin, Scala are all first-rate languages, IMHO. And with projects like Quarkus you can more easily build native executables that bundle the JRE and make distribution very Rust-like. Another environment I like specifically for async operation, but mostly by reputation, is Erlang and it's BEAM VM. Erlang itself is such an interesting language, being dynamic, functional, immutable, without traditional control structures (!) but relying heavily on recursion and pattern matching, and of course the "actor model" and extremely lightweight "processes" was invented here (and promptly ported to elsewhere, as with Akka). It was also created by one of the nicest human beings I've ever experienced, Joe Armstrong, may he rest in peace. IANALL so YMMV.
- kaba0 4y ago> Plus there is OpenJDK I'm not sure what you mean here, but if you refer to OracleJDK here then there is basically only OpenJDK for quite some time now -- OracleJDK is just an (optionally) paid support version of the same codebase. Also, most other vendors are pretty much just tiny patched OpenJDKs also, with some niche exceptions.
- ekidd 4y ago> Most of the pain here come from the unholy trifactor: combining async, lifetimes and dynamic dispatch with trait object closures; which is indeed very awkward in practice. Even in regular Rust, trying to get too clever with lifetimes can cause serious pain. The usual culprit is complex code that tries to never allocate memory. "Oh, well this closure borrows this parameter from the parent function, and then stores a reference to it in this stack-based structure inside the closure, and then we pass everything by reference to this higher order function..." Just say no. If you add async to the mix, then you need to keep your ownership simple. If you have a long-running async function, then pass parameters by value! If you have a polymorphic async function, then return your result in a Box. (This also breaks up the generated state machine used to implement async functions, and can reduce binary size.) So much of this pain is caused by premature optimization. In C++, if you get to clever, you eventually make a mistake and segfault. In Rust, if you get too clever, it eventually becomes impossible to satisfy the borrow checker. Most of the time, the solution is to be less clever. Async Rust was a fascinating experiment, but in practice, I think it turned out to be a tool that's best used conservatively. I've written some very stable and high-performance production code using async Rust. But I keep it simple when I can.
- crabbygrabby 4y agoThis is good advice. Write simple code first. Another generic thing I've come across is, "if the borrow checker is screaming at me when I hit a patch of code, I probably have a design flaw not a programming problem". I've seen a lot of new people also prematurely over generalizing code. I know it sounds terrible to say, but if you probably aren't going to reuse it, you really don't have to prepare your code base just in case you might later. Decide that later and move along...
- FpUser 4y ago>"if the borrow checker is screaming at me when I hit a patch of code, I probably have a design flaw not a programming problem". This reminds me: "you are holding it wrong"
- 4y ago
- cmrdporcupine 4y agoWow, this really speaks to me. I've sunk hundreds of hours in the last few weeks into a tokio-based thing using a bunch of async and ... what a nightmare. It truly is half-baked. And yet it seems like so many things have tied themselves to this async/tokio mast. It's not a good look for Rust.
- Klonoar 4y ago>There are almost no active / popular libraries related to network IO that haven't switched over. To extend this with a slight caveat: for many of those network IO libraries, there's still often a sync alternative. e.g, ureq in place of reqwest works for many use-cases and doesn't bring in an entire tokio runtime for a blocking request. You can find sync DB libraries. Some other crates have started to catch on and offer them as a feature-enabled adapter (e.g, Sentry does this and can use ureq in the background). I feel like a real problem, though, is that there's a division of eyeballs across these boundaries. I would chip in to funding work on a "reqwest-non-tokio-adapter" or something that utilizes all the same reqwest types, but avoids Tokio. (And I like Tokio! I'm basing a new project on it as we speak. I just cringe every time I need to use reqwest::blocking because of something the sync alternatives haven't gotten around to implementing.)
- duped 4y agoThe last thing I would take away from async/await in Rust is that it's "half baked." It's incredibly deeply thought out with years of RFCs, great contribution work that required both low level implementations in nightly and creating extensions to the memory model, and extensive bike shedding and discussion with the community on surface APIs.
- pclmulqdq 4y agoLike a lot of things in Rust, it's incredibly well thought out but its implementation just isn't complete to the point that it is usable by other programmers. A lot of Rust libraries and components of the Rust STL are "half baked" in this way.
- Hirrolot 4y agoasync/.await in Rust is a perfect example of "code duplication" in the language core. Okay, you have a nice syntax for performing do-notation on futures, but what about iterators, streams, etc? We have generators in nightly and some third-party macros for streams, which sucks. A proper algebraic effect system could resolve the problem. You can take a look at Koka to see how elegantly it abstracts common control flow patterns using the concept of an algebraic effect.
- lmm 4y agoAlgebraic effects are still a research area - no mainstream language has them fully working - but basic Haskell-style higher-kinded types should not be so much of a stretch, and are a necessity if you want reusable libraries for this kind of stuff.
- inejge 4y agoA proper algebraic effect system could resolve the problem. It could also take as long as the async MVP itself to get off the ground, and you can't know if it wouldn't hit its own snags when deployed at scale. (From long ago, I remember the "non-parametric dropck" RFC as an illustration of a neat concept colliding with reality.) Languages with mainstream aspirations evolve under greater pressure. I don't know this, but I strongly suspect that async was necessary for Rust to gain acceptance at, say, Amazon. So it couldn't practically have been designed with a wide-open timeframe. The result is what we have; one can uncharitably call it "half-baked" and tut-tut about the sharp edges, but it's workable if you know what to avoid. (But it's so tempting to imagine what could have been, eh?)
- api 4y agoRust is like C++ in the following way: it has many features and a complex type system, but that doesn't mean you should use all its features all the time! Dancing around with lifetimes can be premature optimization. Yes you can write very efficient code that way but if you find yourself spending tons of time fighting the borrow checker you might be overdoing it. I tend to use Arc<> a lot in async code. It makes things relatively straightforward and easy to reason about. Mixing lifetimes with async is probably the most confusing thing you can possibly do.
- Hirrolot 4y ago> Rust is like C++ in the following way: it has many features and a complex type system, but that doesn't mean you should use all its features all the time! ... Until third-party libraries push you to use specific features.
- api 4y agoI get the async frustration, but keep in mind that there are really only three possible ways of handling this: (1) Async in the core language and do async I/O. (2) A fat runtime like Go that implements lightweight concurrency. (3) Everyone hand-rolls their own implementations of select, kqueue, epoll, etc. loops as well as optimizations like io_uring for every single application. Rust chose (1) because (2) is off the table as it's a systems language and (3) is more painful and bug-prone than (1). As for how to implement async: I am having trouble thinking of a significantly better model than Rust and the fact that the Rust community hasn't dramatically improved async shows that a bunch of people who are way smarter than me about languages and type systems are also having a tough time here. Doing async natively in a systems language with virtually no inherent overhead is some seriously difficult stuff. I personally think the biggest oops with Rust async is the absence of a runtime in the standard library, forcing everyone to pick a third party library and causing fragmentation around which one to use. Tokio seems like the clear winner but having played with both Tokio and Smol I really think the latter is better designed. Tokio does not implement structured concurrency and makes dealing with lifetimes harder. If we used Smol there would be less of a temptation to just throw Arc<> everywhere and less memory leak bugs around forgotten-about tasks. (For those curious about the latter: Smol JoinHandles abort when dropped while Tokio just forgets about tasks and leaves them running when handles are dropped.)
- jjtheblunt 4y ago> There are almost no active / popular libraries related to network IO that haven't switched over. wouldn't Go afficianados say Go is a counterexample right under every Rust programmer's nose?
- jjnoakes 4y agoHow is go a counterexample to the rust library situation?
- jjtheblunt 4y agonot to the rust library situation, but it made me wonder, since Rust does not even presume a stdlib, if a standard library with green thread aware implementations, as in Go, is possible in Rust. (i definitely could have worded that better up above, to your point!)
- SquibblesRedux 4y agoI do wish async would go away and never come back -- in every language. Some days I long for the NMI: non-maskable interrupt. I have been coding since the late 70s -- the latter half of the last century. After all I have seen, async feels wrong.
- lelanthran 4y agoI feel you, I really do, but it has it's place. Quite often you really do want to execute multiple different things in the background and wait for all of them to return before proceeding. In pseudocode: 1. var logResults = Background writeLogServiceStarted() // Sent to different machine 2. var authoResults = Background performAuthorisation() // Perform by 3rd party 3. var userSettings = Background getUserSettings(request.currentUser) // Stored in DB 4. var results = Background executeQuery(request.query, authoResults) // Different DB 5. var response = Background generateResponse(results, userSettings) 6. wait (logResults, response) 7. transmitResponse (response) The current async/await solution doesn't really make this as clear as the above though: The code is littered with some form of unwrapping/wrapping at every step hiding the actual intention, the call stack is marked as async making it hard to figure out where and when a sync function can make a call, etc.
- native_samples 4y agoThe JVM guys are working on that with Loom, and, because of its multi-language nature, that can be brought across to many other languages too. Including, oddly, Rust, because Rust compiles with LLVM and GraalVM has a Truffle interpreter for Rust. I doubt anyone would actually want to run an app that way today especially as it's kind of cutting edge stuff and the Rust ecosystem is forcing async anyway, but in principle you could run a non-async Rust server on the JVM with millions of lightweight threads. It'd preserve the safety properties of the language and even the memory layouts, because Truffle doesn't force GC or Java-style memory layouts on the languages it runs. You can even AOT compile stuff but that requires the enterprise edition.
- oxff 4y agoThis isn't really an easy async programming issue the author was having. I mostly do server backends I'd do with Java or Node with Rust and its fine.
- hintymad 4y agoA naive question: do I have to use async to build a web service or RPC service? If I code in Java, I rarely need to worry about async since a service framework will take care of concurrency. I may throw in thread local and a scheduler here and there to handle some shared context or background tasks, or using some bounded queue to manage some boutique concurrency. Will using Rust be similar? Or I’d have to know all the async as people mentioned in this thread?
- formerly_proven 4y agoNo, you don't. Threaded HTTP servers and standard, blocking network clients are available, just not that en-vogue. The very highest performance servers and clients will be async, because threading doesn't get you there, but then again, you (as in: application developer) almost never need that kind of performance in your HTTP stack anyway (this does not make using Rust for such a project pointless in any way).
- otabdeveloper4 4y agoYou don't need async. It provides no performance benefits if you're not using a garbage-collected runtime.
- drogus 4y agoThe last paragraph is the key. Just use `Arc` or `Arc<Mutex>`, it will be fast enough. That's why my first article on a new blog is precisely about that: https://itsallaboutthebit.com/arc-mutex/ https://itsallaboutthebit.com/arc-mutex/
- withoutboats2 4y ago> Async support is incredibly half-baked. It was released as an MVP, but that MVP has not improved notably in almost three years. As the primary mover of the MVP (who stopped working on Rust shortly after it was launched), I'm really sad to see this. I certainly didn't imagine that 3 years later, none of the next steps past the MVP would have even made it into nightly. I don't want to speculate as to why this is. I also recommend avoiding async if you don't need. Unfortunately for people who don't need it but do want to do a bit of networking, a huge part of the energy behind Rust is in cloud data plane applications, which do need it.
- mwcampbell 4y ago> As the primary mover of the MVP (who stopped working on Rust shortly after it was launched) At the risk of rehashing something that has already been discussed to death, did you stop working on Rust because of the difficulty of launching that MVP? I imagine that all of the arguments involved, particularly about things that are prone to bike-shedding like the await syntax, could be exhausting. Any idea where we should send money to get things moving again? Lack of money is always the biggest problem in open source, right? FWIW, I only started seriously using Rust in the past year. I quietly watched and waited while the async/await MVP was being developed. I didn't participate in any discussions. On the one hand, that means I didn't exacerbate any exhausting arguments. On the other hand, I wasn't actively supportive either.
- withoutboats2 4y agoThat's not why I stopped working on Rust. Given that Amazon, Google, Microsoft and others all employ people to work on Rust, lack of money is certainly not the problem. There has never been more money in Rust development.