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.
> 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.
> So much of this pain is caused by premature optimization.
Part of the problem is that Rust itself has such lofty aspirations to do it all (particularly "abstraction without overhead"), and is largely successful at that. So if I compromise on efficiency, it feels like my code is unworthy of the language it's written in.
Also, it's easy to feel that the slightest inefficiency puts us on a slippery slope to the bloat and slowness of something like Electron. I'd like to fight back against that bloat, the upgrade treadmill, and the rapid obsolescence that serves hardware makers but hurts poor people. On the one hand, there was good software in the 80s and 90s that made liberal use of heap-allocated, reference-counted objects with dynamic dispatch. On the other hand, there were slow, bloated desktop applications before the rise of Java, C#, Electron, etc. So I don't know where the right balance is at.
I agree; if you use dynamic `Arc`s almost everywhere in your code, what's the point in a systems PL whose essence is in managing static lifetimes? Sometimes people use Rust just because many other languages suck; they are choosing between two evils: tedious programming in Rust or inadequacies of another language. It should not be like this. This is why I think we need a high-level, no-BS version of Rust.
> This is why I think we need a high-level, no-BS version of Rust.
F# is probably what you're looking for if you want functional-lite programming with a strong type system, but don't want to deal with the pains caused by static resource management.
F# has some nice things in it: I especially like how it handles multiple return values. But compared to the language it is most clearly decended from, OCaml, it ties you to .NET Core and it lacks an ML-like module system and macros.
If you want to use .NET, F# is a fine choice. But otherwise, and especially in the context of an alternative to Rust, I recommend OCaml.
It might sound superficial but the simple fact that there is seemingly a hard dependency[0] on Visual Studio (Code) is a major turnoff for me. I am quite attached to my Vim/command line workflow.
That link includes documentation for building and running F# projects using only the .net core framework. Seems like the main set of headings is misleading, making it sound like you must install VS or (shudder) VSCode to get F# installed.
Vote here for Haskell, which receives great hate on HN because it allows essentially unlimited abstraction.
Meanwhile it has one of the fastest concurrent+GC runtimes, obliterating Ocaml in this regard, and competing handily with JVM or CLR langs.
Finally, it has standard abstractions for concurrency and parallelism that are basically flawless.
Clearly, the hate comes from the association of Haskell with category theory, which is frankly useless/borderline harmful to the working Haskell programmer.
CT was critical to core library devs being able to deliver the main workaday abstractions that make Haskell such an unreasonably productive app languages. But since 2012 you can simply use these main abstractions and enjoy a level of safety and maintainability that rust, c++, ocaml, kotlin, java can't touch.
It's not perfect, but there's been huge improvements of late to tooling (cabal) so there's never been a better time. I really wish the tide could turn, because if you're willing to take on rust for low-level, you're missing out if you don't try haskell for everything else server-side.
> I agree; if you use dynamic `Arc`s almost everywhere in your code, what's the point in a systems PL whose essence is in managing static lifetimes?
You can have the best of both worlds: In many cases, I simply put an object into an Arc, clone that a bunch of times, and pass/store it to wherever it's needed. Then, within loops, I pass a reference to local function calls (instead of cloning the Arc for every call). For any given v: Arc<T>, doing &*v is zero-cost – unlike in many other languages, where you'd have to pass the Arc itself, which would involve atomic increments/decrements without escape analysis.
Hide memory management under a language runtime. Make a single Fn trait/string type instead of multiple ones. Add effect polymorphism to deal with function colours. Remove async/.await and express it using algebraic effects, do the same for streams and iterators.
How it'd be designed and implemented is probably a theme for a separate blog post, not a HN comment.
Functors and modules are nice but do not have the convenience of traits and type classes. The problem with functors and modules is that you have to manually instantiate the right functors and modules when you want to use some operation. Type classes and traits build the right instantiations for you based on the types.
I want to write `toString (1,true)` not `ToStringPair(ToStringInt)(ToStringBool).toString (1,true)`.
> Which parts of Rust would you give up to make this happen? And how would this be implemented?
Traits, maybe? Switch to an OO model more closely aligned to what the majority of developers understand. Dump all of the line-noise-type syntax.
My Own Toy Language (ComingRealSoonNow)^tm that I started designing had exactly one goal - prevent the majority of memory errors, not prevent ALL memory errors. All I wanted was to indicate when/where an object will be destroyed/mutated. That's enough for me.
Actually something like traits (as concept) is what powers COM, WinRT, the basis of Objective-C protocols that influenced interfaces in more common OOP languages.
Also the basis for one programming language that used to be widespread in the enterprise for quick and dirty solutions, VB and its ecosystem of OCX libraries.
It is more widespread that people think, because when many argue about OOP, they miss the full spectrum of how OOP is approached.
Traits seem, to me, a lot like generic functions and methods in CLOS (Common Lisp). It's just that CLOS doesn't bundle a group of generic functions into a trait, they're a la carte.
> Actually something like traits (as concept) is what powers COM, WinRT, the basis of Objective-C protocols that influenced interfaces in more common OOP languages.
So? Giving up Rust-Traits doesn't mean that I will give up on interfaces as a whole.
I don't like the way C++ does interfaces, for example, but that doesn't mean that my ideal language won't have interfaces.
You hit the nail on the head, at least for me. I've found Go to be the right sweet spot between efficiency and productivity. I can't get anything done in Rust. My worst enemy is myself.
I like the thick stdlib in go, but it feels a lot slower even than java. The thin stdlib and giant dependency trees is my primary complaint with rust. I like most of the syntax, but async is not my threading style, I mostly use fine grained threads like rayon provides. I'm currently more excited about julia though. Same dependency hell, but super fast and flexible, and the code looks good.
That is what I usually do. And you can supposedly precompile everything if you just want to deploy a service so restarts are short. I have not done that though.
I just tell everyone, just write Rust like you'd write Scala. Don't try to optimize the crap out of anything until you need it, especially if no one else is going to use your code.
Just make the allocations.
Even if you write it that way it's still significantly more performant and lightweight than the alternatives without much loss in productivity.
This 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...
In industrial machines, there's such a thing as holding it wrong. In some cases, that means getting maimed (that old-school table saw doesn't care whether it chews on wood or flesh) or precluded through a clunkier mode of operation (you need to press these two buttons separated at roughly arm length to make sure that they can't be actioned while you have an arm in the way of the heavy arm-eating chunk of metal). The former is C and unsafe Rust, the later is safe Rust and GC languages.
You're trying to draw a comparison with a consumer product with a design flaw.
It's not the only way, but if you do the tradeoff of going with manual memory management, then you should accept what the tradeoff entails. Essential complexity is non-reducible -- you either manage it automatically through a GC or choose a model that while let you ignore it quite often, will make you think about it in the end.
> If you have a polymorphic async function, then return your result in a Box.
Agreed. Copy, Clone, Box, Arc, RwLock, etc. are your friends. Don't be afraid to use them.
You don't need as much performance as you think you do. Passing things around on the heap is fine for most applications. The Rust compiler is often smarter than you think.
And, when the Rust compiler is dumb or your code is getting stuck, you have code that you can understand and find the hotspot of. Now you can be clever.
I generally go by the principle to borrow if I can without effort, otherwise I think a second of the code is in the critical path or the object is megabytes in size. If neither is the case, I clone without shame. The remaining 0.1% gets my attention (Arc, lifetime annotations, refactoring, or whatever is appropriate).
The important part is perspective. Nobody cares if you clone your command line arguments ten times during startup. As Dijkstra said, optimize the 2% of your program that matter, keep the rest simple
Exactly. Even very experienced people will be easily misled to believe something is performance-sensitive -- computers are ridiculously fast. Like honestly, if it is not a tight loop chances are it seriously doesn't matter what you do. Remember, we write plenty of shell scripts that literally spawn a new process for almost every line, and yet they feel instantaneous.
I'm using Rust since ~2 months. At the beginning I was trying to write my usual C/C++ code and I got completely entangled with lifetimes relationships and the code ended up becoming a mess.
Nowadays as soon as the compiler starts mentioning lifetimes I see that as a warning => I then take a step back and change approach/design => no problems.
I use a little bit of async (I create dedicated variables that are moved to the async functions) and classic multithreading (I use "mpsc" to exchange data between the main & subthreads) and so far I never had a single segfault nor any kind of weird behaviour, which is incredible if compared to some other languages at least for my programming skills :)
> 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.
I've taken to making heavy use of the smallvec and smartstring crates for this. Most lists and strings are small in practice. Using smallvec / smartstring lets you keep most clone() calls allocation-free. This in turn lets you use owned objects, which are easier to reason about - for you and the borrow checker. And you keep a lot of the performance of just passing around references.
I tried to use async rust a couple of years ago, and fell on my face in the process. Most of my rust at the moment is designed to compile to wasm - and then I'm leaning on nodejs for networking and IO. Writing async networked code is oh so much easier to reason about in javascript. When GAT, TAIT and some other language features to fix async land I'll muster up the courage to make another attempt. But rust's progress at fixing these problems feels painfully slow.
However the constant checking if something is on the stack or heap is a big performance problem for smallvec. It results in terrible cpu branch prediction.
I don't see how is it bad for branch prediction. Like, if you use the same smallvec object in a loop and do not insert any elements to it (so that it doesn't allocate on the heap) the branch will always turn in the same direction making it easy and cheap to predict. And I would think that in most use-cases your smallvec object will either remain in one state, not changing in-between.
> So much of this pain is caused by premature optimization.
Rust is normally used only when high performance is of uttermost importance, so it will always attract people who want to optmise everything.
> Most of the time, the solution is to be less clever
I've done it myself and saw performance degrade, as was expected. That's fine, but by that point you might as well just use another language that has none of the hard problems and end up having very similar performance, which is what I did.
> Rust is normally used only when high performance is of uttermost importance, so it will always attract people who want to optimise everything.
Which is not the way to do things. Profile, then optimize.
I'm writing a metaverse client that's heavily multithreaded and can keep a GPU, a dozen CPUs, and a network connection busy. Only some parts have to go fast. The critical parts are:
* The render loop, which is in its own higher-priority thread.
* Blocking the render loop with locks set during GPU content updating, which is supposed to be done in parallel with rendering.
* JPEG 2000 decoding, which eats up too much time and for which 10x faster decoders are available.
* Strategies for deciding which content to load first.
* Strategies for deciding what doesn't have to be drawn.
Those really matter. The rest is either minor, infrequent, or not on the critical path.
I use Tracy to let me watch and zoom in on where the time goes in each rendered frame. Unless Tracy says performance is a problem, it doesn't need to be optimized.
Coming from gaming industry i think you might want to measure how far you can go with a single threaded rendering. There is limit of content and code that can be a brick wall later.
Here is an example from SIGGRAPH 2021 where Activision presents how multithreaded rendering looks like: https://youtu.be/9ublsQNbv6I
ps I don't work with Activision its just a public example that illustrates industry practice.
I'm using Rend3/WGPU, where multithreaded rendering is coming, but isn't here yet. Work is underway.[1]
The Rust game dev ecosystem is far enough along for simple games, but not there yet when you need all the performance of which the hardware is capable.
Cool video that matches best practices. Reducing memory footprint is always good and laying out things in memory is also good way to speed things up without changing amount of work.
I have trouble with concept of WGPU. GPUs are complex by themselves to bolt on top any abstraction that is coming from Web. But its just me, its not important since I am not a 3d programmer myself. I am more Engine / CPU optimization guy.
My interest in Rust and this topic is that i would like to see fine grained task parallel systems written in Rust. Instead of systems with separate thread for render that became a bottleneck years ago. I wish you good luck and hope to see a success story about Rust.
WGPU's API is basically Vulkan. It exists mostly to deal with Apple's Metal. Metal has roughly the same feature set as Vulkan, but Apple just had to Think Different and be incompatible. I'm not supporting the Wasm or Android targets. Android and browsers have a different threading model, and I don't want to deal with that at this stage. Linux/Windows/Mac is enough for now.
Thought for the near future - will VR and AR headgear have threads or something more like the processes with some shared shared memory model from Javascript land?
(That video isn't me, it's the Rend3 dev, who also works on WGPU.)
In Zig, we are writing a moderately complex application that calls mmap many times in the first few seconds and never thereafter. We can use every part of the stdlib and most libraries because we can pass in allocators backed by the memory we reserved at startup.
> 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.
I 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.
I do it first and foremost because I dislike the error checking story in Go, and writing Rust for web stuff doesn't really feel too dissimilar to hacking on web frameworks of years past. In fact, I'd argue that web domain stuff is where Rust is relatively mature.
But this all said, there's definitely a point to be made here - Go works fine for so many use-cases, people should use it if they like it or it fits the story better. The idea of "one true language" has never worked out.
Generics were around for some time now, and people don't seem too keen on using them for anything aside from data structure specialisation, because frankly dynamic dispatch via interfaces hits such a sweet spot where you get a lot done with a very minimal overhead. The added syntactic complexity is very rarely justified, and containers is perhaps one of those cases where it is the case. Otherwise, from what I observed— generics in Go were massively over-hyped, but failed to gain the traction that was initially expected from it. I feel like there's a very good reason for that.
Generics as a feature is almost invisible when it exists and is done well, but is a huge pain in the ass if it doesn't. It is mostly needed for libraries, so your average dev won't write them often, but there it is invaluable.
>"I don't understand why so many people lately seem to want to use Rust for web domain stuff."
>"Rust is the new C++. Let's just stick with that."
I write "web domain stuff" in C++ and it is incredibly easy (well for me at least). In C++ I could always use my own styles / paradigms / patterns etc. etc. Not forced to any particular way. And modern C++ is incredibly safe if one wishes.
So if it is bad idea to write "web stuff" in Rust it means it is anything but new C++
That's not my experience. I'm 3 times more productive at writing web apps in C# than in C++ and that is not even taking into account compile times and hot reload and doing unit tests.
Maybe I'm the worst C++ programmer in history and maybe I didn't spent too much time in trying to write web apps in C++ - it was just to test if it's a viable approach - but that was my particular experience.
Aside from doing more boiler plate code, the language being more ceremonious, I find that I miss the tools like frameworks and libraries which are very easy to integrate with each other and which I take it for granted in C#. Probably the story is the same with any other language used for web: Java, JS, Ruby, Python and even the (in)famous PHP.
I think the situation can be much better if someone would make some nicely designed frameworks and libraries, but I guess no one is interested to as C++ is perceived as a "not for web" language. People who are into C++ are generally systems programmers who are not into Web, and people who are into Web are taught they only have to use "web languages".
That is really a shame because for some situations there would be a huge benefit of having performant web apps - scaling out is not always a solution to a performance problem.
>"I didn't spent too much time in trying to write web apps in C++ - it was just to test if it's a viable approach - but that was my particular experience."
I rewrote web apps written in PHP and Python. Those were rather decent size and in my case app specific code was about the same size in C++ as in the other 2. The performance was of orders of magnitude better.
My applications do not contain millions line of code and maybe because of this and the way the code is organized I do not really suffer long compiling times. Usually it is just few seconds. Good enough for me.
Depends on the ecosystem, while I agree with you, at least on Microsoft stack there were always nice tooling to write C++ applications for web apps.
Before .NET we had ATLServer, then C++/CLI.
They also published some frameworks for writing web APIs in C++.
Now, I would also advise C# and then if needed, to call into C++ via P/Invoke (or C++/CLI, C++/WinRT if on Windows), than exposing C++ directly into the wire.
I wrote plenty of vaguely web stuff in C++ at Google. A lot of services at Google are C++. But it's kind of, not how the rest of the industry expects things. And most of that stuff there is now moving to Go.
>And most of that stuff there is now moving to Go.
Go is on the same page performance wise with Java and C#. Sure, if you don't need the best performance, you can move to another language. But, then, why did you start using C++ in the first place?
I really don't believe that this high-level low-level language thing is true. Sure, both C++ and Rust are incredibly expressive and cool languages but they want to give control for low-level details, yet make it very easy to ignore these for the most part. But there will sure come a time when you will have to (may not happen at first write, but will definitely happen at refactor, adding a new feature, etc). Managed languages make such refactors trivial, while C++ and Rust (even with its very advanced type system) make these harder due to you having to rearchitect the whole program from a memory model perspective. Sure, it can be trivial in many cases, but not always.
So all in all, I really don't think that the (long-running) productivity and maintainability of managed languages can be approached by these low-level langs. And that is fine, (thank God) not everything is a dumb CRUD web app, there are very real niches where that low-level detail is a necessity.
>"Managed languages make such refactors trivial, while C++ and Rust (even with its very advanced type system) make these harder due to you having to rearchitect the whole program from a memory model perspective. Sure, it can be trivial in many cases, but not always."
This is not the function of the language but the ability of the developer to properly architect their code.
And tools like Visual C++ / CLion have very advanced refactoring features.
> In C++ I could always use my own styles / paradigms / patterns etc. etc.
That's also one of its major disadvantages, unless you literally rewrite the entire thing when major maintainer changes happen, because otherwise you get a mix of different C++ styles in your codebase which leads to nobody being able to maintain it.
I don't know, I never got heavily on the C++ inheritance bandwagon when it was popular in the late 1990's early 2000's. Rather preferring aggregation.
If you application is actually using the OO parts of the language that way, even without modern C++ its fairly easy to maintain as the different styles/etc area also encapsulated in their classes. Then hopefully the top level is using some kind of message passing interface/whatever to avoid trying to glue everything together into a god class.
AKA, there are a few fairly easy to understand rules that allow people to do their own thing without creating a maintainability nightmare even with very large C++ codebases. If an experienced engineer/architect/etc with a track record of successful C++ projects is in charge during the initial application design/etc you should have a fairly maintainable application.
I'm not sure this is really a C++ thing though, rather being a general engineering thing. If the main architecture is well thought out and understandable, a lot of sins can be burred in places where they can't create application wide chaos.
There are a lot of things that can make C++ applications suck, but you don't tend to hear about the success stories on your favorite board, those systems silently do their job. So many of the things people rail about with C and C++ simply aren't problems when appropriate engineer culture is maintained. AKA, having solid unit tests for most of the base classes being agragated, means that running them under various address sanitizer/etc tools will find the errors that aren't picked up by static analysis tools/etc.
Yes, sometimes things sneak by, but i'm not sure there are any languages java/rust/etc that solve that problem completely.
> because otherwise you get a mix of different C++ styles in your codebase which leads to nobody being able to maintain it.
I'd be really interested in meeting someone who has enough mental flexibility to learn C++ but not enough to accommodate different code styles in a single codebase.
Modern C++ isn't really safe. Safe means the compiler catches you, generally C++ compilers don't. Trivial example is iterator invalidation -- even with all warnings and errors on compilers don't catch it.
Can we cut it out with the hyperbole? Using "It Isn't Really Safe Unless It Is Written In My Favourite Niche Language" as an argument is .. well ... ridiculous. You can extend that argument to any practical programming language.
So ... Rust ... "Isn't Really Safe" because you can get into a point where memory is corrupted, where your application deadlocks, or threads starve, etc.
Haskell ... "Isn't Really Safe" because it is not possible to formally verify the logic.
Etc, ad infinitum ...
Maybe go for "Not As Safe As". After all, I don't even like C++ (see my comment history), but it's certainly possible (and not very hard) to get about 90% of Rust safety in C++.
Safety is not a binary, it's on a spectrum. Saying that a language is either safe or unsafe implies that the "safe" language is actually safe while the "unsafe" language is completely deadly. That's certainly not true.
> Rust ... "Isn't Really Safe" because you can get into a point where memory is corrupted
i dare you to find memory corruption in safe rust that isn't already on an issue tracker.
> Saying that a language is either safe or unsafe implies that the "safe" language is actually safe while the "unsafe" language is completely deadly.
tell that to the people who got owned by https://googleprojectzero.blogspot.com/2021/12/a-deep-dive-i... , most likely human rights activists targeted by less-than-savory regimes. memory corruption bugs are so frequent that it is possible for nso group to sell to pretty much any regime, and not forced to be classified as top secret information.
(To be clear, I’m not saying that there’s no issues that aren’t in the tracker yet. But there are a bunch of them that are. More will absolutely be found as time goes on, that’s just how these things are.)
Safety isn't an all or nothing thing, and in most cases, rigorous safety comes with tradeoffs. In some applications those tradeoffs are worth it, and in others they're not. A small project probably doesn't need to be provably correct, because it's easy enough to analyze it, and you aren't gaining much if the more rigorous language is unwieldy (they often are); similarly, a large enterprise project is harder to analyze, and has larger costs if there are CVEs, so a safer language is probably a good fit. Note that this doesn't necessarily mean rust, though -- garbage collected languages sit here, too. Rust sits in the niche of "big enough and / or critical enough to justify rigorous safety", and "high performance really matters".
We use rust for web dev purely for the type safety. Having business logic encoded as a state machine in an enum with compile time checked matches makes the world of difference
Those benchmarks are highly gamed. Don't trust them. Stuff like hardcoded response length ... Also, they use http and DB pipelining, which hardly reflects most real world usecases.
I find that part a hurdle, actually, despite loving Rust. I'm struggling with magic tools like wasm-pack that take over the build process. Cargo is fine – I know what Cargo does and how and why it calls rustc. But finding out what wasm-pack and friends do has been an uphill battle for me.
Why can't I just compile to wasm32 with Cargo? What's missing?
As far as I know you can using the wasm32-unknown-unknown target. I think wasm-pack does extra stuff like supporting different targets like a nodejs module or a webpack compatible format, which is outside of the scope of something like cargo imo.
Why? 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.
I 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.
Yea 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...
Rust does not in general have an order of magnitude advantage over Go. 2 or 3 would be closer, and that's with some nontrivial attention paid to optimization, not "you write Rust and it's automatically always faster".
Super high-end stuff can outclass Go by that much, like if you're seriously using DPDK or something, and in those cases I strongly recommend Rust over Go. There some other noches like that. But in general, it's not a factor of magnitude.
It really depends. I agree it's not always an entire order of magnitude didn't mean to paint that picture for passerbys who don't know better. Not trying to touch the "this lang vs that lang performance" conversation with a ten foot pole...
10x improvement over Go is a reasonable expectation, in a limited set of environments, generally at a scale where you're looking at using every last bit of a 32-core or 64-core machine and doing a lot of memory work. You should also expect to be spending a lot of time optimizing that Rust to get there. But when you need that level of performance, you also basically made a mistake starting with Go in the first place.
However, by far the bigger problem is people thinking their little web service serving out 5 requests a second requires that level of optimization when in fact even a Go implementation will use <1% of the CPU and no other resources to speak of. Dynamic scripting languages, IMHO, have skewed a lot of people's performance views. Go is already more power than most problems need, in terms of raw performance, and it's only a sliver, a niche where that's the difference between a Go vs. Rust choice.
It's a delicate understanding, but an important one for a professional.
I would not suggest using DPDK with Rust, unless you want to write a lot of stuff for yourself and wrangle with some tricky `unsafe` code. Sticking to C or C++ will make your life a lot easier.
Pretty sure you could hire 5 rust developers pretty easily. A lot of people in the rust community are dying to find a job that will let them write rust professionally...
Perhaps in TechEmpower benchmarks, which to be fair aren't the worst way of taking frameworks out for a run to see what they can do, but at the end of the day they're very synthetic workloads. Interesting applications are piles of business logic where a slightly faster framework doesn't really buy you much.
Anecdata are all I can provide, but the equivalent (Google) batch job in C++ vs. Java is often an order of magnitude better for a variety of reasons. It might only be 0.5-3x faster, but it also uses less cores and less memory to do the same amount of work.
Having written C++ and Java services, the median C++ service is performs better than the median Java service. Some of that is less pointer chasing, some of it is better libraries. Some services will be slow no matter what you write them in. There's too many variables to quantify, and "performs better" is a real load bearing term. Sometimes it's less CPU, memory. Sometimes it's latency.
I still primarily work on Java services, and they perform pretty well. That said, the operational dynamism of Java services is a real thorn in my side. Beyond the GC, there's a lot going on in the runtime including JIT that just makes JVM services less predictable. Even classloading is a source of unpredictability, unless you eagerly load classes.
FWIW, I think Go does a pretty good job being predictable too. AOT compilation helps there, you're not worrying about new tasks needing to JIT. Go mostly worries about GC (often irrelevant), warmup of state (like TCP connections), and avoiding footguns that leak Goroutines. :) At least Go's footguns rarely blow your whole leg off.
>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.
I don't get why are you so dismissive about web programming?
The Go libraries are good because it's easy to write good libraries in Go. And it's easy to write good libraries in Go, because of design decisions. Same for C#, F#, Java, Python and more.
Go's ecosystem is amazingly stable. Its rich standard library helps, but in general, there's a lot of attention to backward compatibility.
Once an application has been written in Go, updating dependencies or using a more recent compiler version is a breeze. Nothing breaks.
Rust code on the other hand comes with a high maintenance cost. The ecosystem is still very unstable. Core dependencies constantly have breaking API changes, or get abandoned/deprecated/superseded. Keeping everything up to date is not trivial and very time consuming.
I abandoned projects due to this, and others use dependencies with known vulnerabilities, that may not even compile any more at some point. But dealing with changes in Rust dependencies instead of the actual application logic is not fun.
So, for a project that has to be maintained long-term, I would choose Go anytime. Productivity is so much higher, especially when including maintenance cost.
> The ecosystem is still very unstable. Core dependencies constantly have breaking API changes, or get abandoned/deprecated/superseded. Keeping everything up to date is not trivial and very time consuming.
I'm only a hobby coder but this has hit me a few times. I suspect this will level out in time, though.
This has been discussed on the Rust forums. There are too many widely used packages that are stuck at version 0.x, with no stability guarantee. The basic HTTP library, "hyper", is at 0.14.19, and it's had breaking changes more than once. 48,300,708 downloads.
* "image": 0.24.2 (Read and write common image formats, 7,257,076 downloads)
Rust needs a push to get everything with more than a million downloads up to version 1.x. Then the semantic versioning rules are supposed to require no breaking changes for existing code without changing the major version number.
> The problem is that the entire ecosystem has completely shifted to async. There are almost no active / popular libraries related to network IO
The ecosystem, or the ecosystem where network IO is a thing? Surely that's just a corner of the Rust library ecosystem. I have almost never used network IO (databases, http,...) in 20 years of programming, and zero times in Rust.
There is a big (and might I say extremely comfortable) world of programming everything is on one machine, and things are CPU instead of IO bound.
Completely 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.
I’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?
Rust marks cross-thread shared memory as immutable in the general case, and allows you to define your own shared mutability constructs out of primitives like mutexes, atomics, and UnsafeCell. As a result you don't get rope to hang yourself with by default, but atomic orderings are more than enough rope to devise incorrect synchronizations (especially with more than 2 threads or memory locations). To quote an earlier post of mine:
In terms of shared-memory threading concurrency, Send and Sync, and the distinction between &T and &Mutex<T> and &mut T, were a revelation when I first learned them. It was a principled approach to shared-memory threading, with Send/Sync banning nearly all of the confusing and buggy entangled-state codebases I've seen and continue to see in C++ (much to my frustration and exasperation), and &Mutex<T> providing a cleaner alternative design (there's an excellent article on its design at http://cliffle.com/blog/rust-mutexes/).
My favorite simple concurrent data structure is https://docs.rs/triple_buffer/latest/triple_buffer/struct.Tr.... It beautifully demonstrates how you can achieve principled shared mutability, by defining two "handle" types (living on different threads), each carrying thread-local state (not TLS) and a pointer to shared memory, and only allowing each handle to access shared memory in a particular way. This statically prevents one thread from calling a method intended to run on another thread, or accessing fields local to another thread (since the methods and fields now live on the other handle). It also demonstrates the complexity of reasoning about lock-free algorithms (https://github.com/HadrienG2/triple-buffer/issues/14).
I find that writing C++ code the Rust way eliminates data races practically as effectively as writing Rust code upfront, but C++ makes the Rust way of thread-safe code extra work (no Mutex<T> unless you make one yourself, and you have to simulate &(T: Sync) yourself using T const* coupled with mutable atomic/mutex fields), whereas the happy path of threaded C++ (raw non-Arc pointers to shared mutable memory) leads to pervasive data races caused by missing or incorrect mutex locking or atomic synchronization.
Re: fearless concurrency... Would Rust prevent you in general from writing code that could deadlock, btw?
Thread1: takes lock A, ..., tries to take lock B
Thread2: takes lock B, ..., tries to take lock A
Looks like you should be able to pass Mutex<A> and Mutex<B> to both threads otherwise what's the point of mutex if there's no way to share data protected by it, so it doesn't look like it prevents you from hitting this scenario.
No, Rust doesn't prevent deadlocks, a deadlock is safe (it isn't what you wanted, but it's safe). There are well-known strategies to avoid deadlock (in any language)
In the trivial example you gave, one strategy just insists we take locks in alphabetical order. Thread 2 can't take lock A, because it already has lock B and that's not the correct order.
No, deadlock-free code requires some additional structure. There is no general way that I know of to prevent deadlocks in any software with non-trivial lock graphs, but there are standard techniques to detect deadlocks programmatically so that they can be broken and resolved. OLTP databases figured out how to do this decades ago, but those techniques are expensive for general purpose programming.
The common method for deadlock-free code is roughly that when a thread is required to wait on a lock owned by a second thread, it checks to see if the second thread is waiting on a lock already owned by the first thread. This requires that the lock graph essentially be a high-performance and concurrent global structure.
Locks like this can be expensive, particularly under high concurrency or contention, so they aren't used for most software. If you can fit your software in a simpler model e.g. where locks are singular or only acquired as a DAG, then much higher performance options are available that don't require deadlock detection.
In the general case you're right, it's equivalent to the halting problem. The outline of the proof by reduction: set up two communicating processes in a way that will deadlock iff a particular loop in one process fails to terminate. So if you had a deadlock detector for arbitrary communicating processes, you could use turn it into a termination detector for arbitrary loops.
Yes, deadlock-free lock systems are an ordinary part of OLTP database engines. They don't prevent deadlocks per se so much as detect them and dynamically resolve them.
The mechanism is costly but elegant. If a lock you are trying to acquire is owned by another thread, you inspect the locks you own to determine if that thread is waiting on one of your locks. When a deadlock is detected, there are several strategies to automatically resolve it e.g. rolling back one of the threads to a point where forward progress can be safely serialized.
No one wants to use these mechanics for ordinary code, due to their cost. For the fashionable thread-per-core software architectures, deadlocks aren't something you commonly have to worry about.
There are two kinds of threading bugs: deadlocks which are easy to detect and race conditions which are far more difficult to detect and to fix.
AFAIK Rust helps with the latter not with the former which is a very big improvement (much more than if it was the other way round)
Just to add, AFAIK Rust only prevents data races, not race conditions in general. Which is still a huge help, but concurrency is still hard without a much more restricted model.
Yeah, but it doesn't help on shared-memory process concurrency, and we all know that in 2022 the best way to ensure secure software is to go back to processes.
I 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.
"fallen from light" would mean breaking it's safety promises. No promises have been broken, the async developer experience is just less than a silver bullet and needs a lot of work. If you're so disappointed by that fact that you'd say rust has "fallen from light" then you've not been paying attention at all or your expectations need serious calibration.
Yes, Arc for structured data passed between threads, atomics for smaller things like counters job queue length. Of course the fastest synchronization primitive is nothing at all :)
> 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.
> 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.
That'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.
> 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.
Wow, 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.
>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.)
> 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.
Anecdotally, 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.
I'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.
I 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.
The 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.
In 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.
For example, in your typical web application a Goroutine is spawned for every incoming connection. This basically gives you a dedicated runtime for each simultaneous connection. In Node or Python, this would be the equivalent of starting a whole new process for every request. But in Go, there's very little cost to doing it this way.
The advantage is that each connection basically never blocks waiting for another user.
In Node and Python, we talk about handling tens to hundreds of requests per second per server.
In Go, we talk about handling thousands to tens of thousands per second. It's just orders of magnitude more throughput.
Can't comment on Python, but I once worked on a Node.js codebase that could only handle tens of requests per second, it was highly unusual and doing some incredibly dumb things like pinging out to Redis to check for DDOS protection counters before starting to serve any request.
Node is more susceptible to poor code and out of band IO slowing it down, but the V8 runtime itself is cpp and simple services are close to just writing decorators over a cpp webserver.
It's obscenely fast and good enough for most purposes.
Scala. With the functional effect systems, like Cats Effect or ZIO, you get superpowers.
Not only can write programs that are "async", but you also get easy retries (and other tricks), safe refactorability (because of its Pure FP nature), reliable and painless resource management and some other goodies like STM (Software transactional memory).
Haskell has always been the best. Go's did the same as Haskell. In either case IO is automatically async. If you want concurrency you create very light weight threads. This approach works very well for most use cases, but when you want the best performance possible the light weight threads can still be too much. Zig is going for a lowest possible overhead approach like Rust but has an interesting take: https://kristoff.it/blog/zig-colorblind-async-await/
That's definitely a description of GHC Haskell. All network IO in Haskell goes through a subsystem called the IO manager, which makes use of platform appropriate high-performance non-blocking APIs. (Actually, I think windows isn't getting an IOCP implementation until the next major release, but Windows has always been a bit undersupported by GHC.)
The nice thing about this is... You don't have to care. Haskell is a good enough programming language to just put the platform-specific non-blocking hell APIs in a library and let you write code that looks linear and blocking. If you want more control you can get down to the low level non-blocking APIs, but that's usually not going to be worth the trouble.
Yeah, many people don't even see a reference about Haskell IO being async. That's what the "automatic" part is doing.
Haskell IO is basically as async as Javascript, as in every operation is fully asynchronous, you need to call some foreign function if you want otherwise. Except that you have parallelism and can have concurrency too added if you want.
Interestingly, 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.
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.
You must be young. Java was not always distributed like this, and although it was "open source" few people compiled it, and the JRE and JDK was distributed by Sun (then Oracle) primarily through a user interactive web UI. The OpenJDK existed alongside this for some time, but then supplanted the proprietary binaries. That was a relief because it meant Java was actually (not just theoretically) open source now, which meant it was safe from deprecation, disablement, and all the other negative aspects of control that come with de facto proprietary software.
I cannot, for the life of me, recommend Elixir enough! You write your code without every thinking of words like "async" or "await" and the VM handles it for you!
So this. You have to learn to think differently about your problem. But then when you do, so many of these other issues just go away. A small amount of our product offering is implemented in Elixir. I wish more of it was. It’s my favorite part of the whole thing.
Any language which is dataflow based will be great in an async context. Async is hard in most languages because they are imperative, which stands in stark contrast to the whole idea of asynchrony. This whole exercise of adding async features to various imperative languages is coming at the problem from the wrong way around in my opinion. It's a recognition that async programming is hard in imperative languages, and the thought is that maybe this could be made better with more language features. But the sad truth is that async features clash hard with the imperative nature of most mainstream languages.
Server workloads are different from client workloads here, and network/IO heavy workloads are different from CPU heavy workloads. You'll get tend to get misleading advice from people only familiar with one of them. Especially if the client is an asymmetric architecture like ARM has and Intel is moving to.
Swift's design is made to be good for IO workloads on smaller clients, though it hasn't got as many tools for the other end.
It 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.
But 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.
True, 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):
A 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?
No, 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).
The 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/
I 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.
The 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.
Like 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.
async/.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.
Algebraic 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.
A 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?)
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!
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.
> 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.
I 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.)
> 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!
I have come to the same opinion about Scala. Stick to the basics (i.e. the Scala Book[0] and doesn't even have to be all of it) and it is a joy to use.
not 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!)
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.