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.
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.
Right but you dont have todo this everywhere, just some sporadic use where it makes life a lot easier. It doesn't have to be all or nothing.
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.
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.
[0] https://docs.microsoft.com/en-us/dotnet/fsharp/get-started/i...
https://github.com/ionide/Ionide-vim
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.
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.
How it'd be designed and implemented is probably a theme for a separate blog post, not a HN comment.
And then the O in OCaml is all about OOP.
I want to write `toString (1,true)` not `ToStringPair(ToStringInt)(ToStringBool).toString (1,true)`.
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.
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.
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.
F# to the rescue. :)
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.
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...
This reminds me: "you are holding it wrong"
You're trying to draw a comparison with a consumer product with a design flaw.
Not at all. I just do not like to dance around bonfire with tam-tam and being told that this is the one and only way
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.
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
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 :)
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.
https://crates.io/crates/smallvec / https://crates.io/crates/smartstring
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.
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.
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.
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.)
I dont work with VR/AR myself so I dont know.
All that is actually needed is for it to be more work to get it wrong than to get it right.
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 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.
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.
I hope not, it might have some generics now, but still has a lot to catch up with.
I surely don't miss coding in Java 5 (2004).
> Rust is the new C++
Rust might become the new C++, and while being safe by default is great, there are plenty of C++ use cases where Rust has zero presence in 2022.
The good reason is the target demographics as Rob Pikes puts it, people that don't care about learning what programming language can be like.
>"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++
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 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.
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 run my own company, prefer to count money and Have no shortage of work - so could not care less about what "the rest of the industry thinks".
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?
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.
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.
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.
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.
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.
If you are not embarrassed by your old code, you have not learned anything since.
I use other libs of course but those are domain specific and are solving non web problems
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.
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.
can't you just mess around with /proc/self/mem :)
https://github.com/rust-lang/rust/issues/32670
(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".
Possibly a combination of type safety + speed in one bucket makes the difference
Mostly because of performance. Looking at latest Techempower benchmark, stuff written in Rust is 50% faster than stuff written in C# or Java. [0]
If that difference in performance and the tradeoffs to obtain it are worth, that's for everybody to decide.
Have a look here:
https://web-frameworks-benchmark.netlify.app/result?f=axum,g...
wasm-pack build --target web
And it builds a little wasm file and some js shim, and that's all it takes to get a Rust website up and running.Why can't I just compile to wasm32 with Cargo? What's missing?
To be fair, I usually prefer to use go as well, lately though rust is more appealing.
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.
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...
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.
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.
https://web-frameworks-benchmark.netlify.app/result?f=axum,g...
I've done some benchmarking with Actic, Axum and Go (std lib). Actix was top, Axum about 85 % of Actix and Go about 75 % of Actix.
For a lot of web stuff you need throughput more than you need latency "fast".
In order to do throughput well you need concurrency.
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.
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.
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.
I'm only a hobby coder but this has hit me a few times. I suspect this will level out in time, though.
* "image": 0.24.2 (Read and write common image formats, 7,257,076 downloads)
* "glam": 0.20.5 (Basic 2,3,4D vector and matrix operations, 1,155,849 downloads)
* "cached": 0.34.0 (Cache management, 1,367,883 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 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.
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?
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.
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.
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.
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.
I wonder if it is similar to the halting problem. Can deadlocks even be prevented in theory?
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.
Once upon a time, the whole promise of Rust was “fearless concurrency”.
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.
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.
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.
And yet it seems like so many things have tied themselves to this async/tokio mast. It's not a good look for Rust.
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.)
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.
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.
I have been coding since the late 70s -- the latter half of the last century. After all I have seen, async feels wrong.
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.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.
See: https://www.fastify.io/benchmarks/
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.
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).
It's really that good.
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.
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.
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.
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.
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.
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.
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 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.
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?)
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.
... Until third-party libraries push you to use specific features.
(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.)
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.
[0]: https://docs.scala-lang.org/scala3/book/introduction.html
wouldn't Go afficianados say Go is a counterexample right under every Rust programmer's nose?
(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.