Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Async Rust doesn't have to be hard (itsallaboutthebit.com)
277 points by drogus on June 3, 2022 | hide | past | favorite | 188 comments


I learned Rust by writing a (fuse-based) filesystem [0]...about 30k lines iirc. Rust was challenging, but not crazy difficult (it helps that I have a C++ background). I loved it. Development was slower than a dynamic language, but faster than C++, and most importantly, I felt safe. It's a really solid language.

However, when I took a look at async Rust, it really did appear to be a mess. I have substantial experience with Python async/await (which is also a mess), so I'm not unfamiliar with the async concepts. Honestly, I think it's the idea of an event loop in a compiled + non-memory-managed language. You really have to think hard about where objects are living and for how long, and combine that with the illusory world of how async/await appears to work (versus how it actually works), it gets hard to conceptualize. Maybe I just didn't spend enough time with it to feel comfortable with it, but that's my hot take.

Imo, Go does performant concurrency right. Rust would be smart to adopt what Go offers.

0. https://github.com/amoffat/supertag


Exactly. And worse, async in Rust is viral, because any code outside of async doing I/O is not good to use inside of async. And it gets worse: making everything async is not a good option. Effectively, it seems there is no viable answer: you need two of everything.

Go does do concurrency well, but unfortunately it’s only able to do that because of opinionated decisions that a language with Rust’s design goals cannot really make. They also have tradeoffs that I don’t think Rust developers would accept, like making calls into C functions slower. So I feel as though Rust async is at an impasse. It can be made much better, but it feels like the improvements will come at increasing complexity in the language, compiler and ecosystem. Meanwhile, the ideal end state seems like it will still have a lot of annoyances, such as the need to tirelessly duplicate anything that needs I/O for sync and async.

I almost kind of wish Rust would just drop async, as the rest of Rust is much better, having only minor issues that are very much fixable IMO. Instead the ecosystem is accelerating into it, and now it’s hard to avoid for some use cases.

I hope it all works out, because Rust is good and I’ve been advocating for its adoption at work and continually trying to adopt it elsewhere. But the issues with async are very dire, imo. Async rust is cool, but it isn’t what I would consider to be a similar level of robust, thoughtful design.


The viral aspect is the thing which really annoys me as well about it: I have some apps that make one or two single requests to DBs in certain configurations, and I've had to end up using async for those parts (up to main() obviously, although not all code in the apps needs to be aware of async thankfully) due to many crates needing/using async now for this (which may well be good/fine for other peoples' heavy usage, but not really for my usage).

However, it means that even when these requests are not being done (and might never be for the running of the apps, as it's user-configured what the apps do and whether they make DB requests occasionally), these apps end up having some (it may not be much, but it's somewhat noticeable) overheads, i.e. coreCount threads are always created by the async infrastructure (despite being a single-threaded app in one of the apps cases), call stacks are deeper even though async is not really being used (although due to main() being async effectively that changes everything), meaning more memory usage for those threads's stacks (which is somewhat ironic, as that's one of the points of utilising async for heavy IO requests - reducing memory usage! - but in reverse it hurts my use cases a tiny bit).

Edit: I tried to use things like pollster (in an attempt to significantly isolate and limit the async usage to just where it was needed), and it wouldn't work for my use cases.

I'm on the verge of splitting the apps into two parts due to this, but due to the shared state, that would involve additional complexity (RPC or something), which I don't really want to stomach.


You can spin up a single threaded runtime to perform these async calls without the need to "infect" the rest of the program.

The strategy is to spin up a single threaded async runtime and to just perform that call and block on the runtime itself. The easiest way to do that is probably https://github.com/zesterer/pollster

And there really isn't much in there so you didn't need to worry about performance or anything like that.

I recently refactored a colleague's program from async to sync because it's essentially and entirely sequential.

The reason it started its life as async was the reqwest library, which first and foremost provides async methods to perform http requests.

However, tucked away behind a feature flag aptly named `blocking` there is a small API that wraps the async api and allows making sync/blocking calls in a non-async main. And there they employ the same strategy of having a thin async runtime that blocks on completion of the async call.


A multithreaded runtime that is shared for all functions in the app - that other synchronous code then "blocks_on" - will also work. Or e.g. having a cached thread_local tokio runtime.

Btw:

> And there really isn't much in there so you didn't need to worry about performance or anything like that.

Actually you have to! In case you write a program that spawns background threads (with whatever async runtime), and then let your foreground thread interact with that - it will have performance implications since your program now does additional context switches. It might or might not matter for your application, but in general it's rather easy to lose all perf benefits that async code actually might provide by still requiring switches between full threads.


This works with runtime agnostic futures, but you can't do any I/O without requiring a specific runtime. reqwest for example doesn't use a generic block_on, it runs tokio behind the scenes.


You can spawn a simple Tokio I/O runtime with Builder::new_current_thread().with_io().build().

The catch is that it’s not really “simple”, and idk the performance penalty.


I tried things like pollster, and they wouldn't work in my case for reasons I can't remember (but I asked for assistance on the Rust Discord and there didn't seem to be ways around it).


I worked on a rust project that had one thread with a httpserver all async, and another thread with some mqtt client all sync. Communicate over channel. No big deal.


>I almost kind of wish Rust would just drop async, as the rest of Rust is much better, having only minor issues that are very much fixable IMO. Instead the ecosystem is accelerating into it, and now it’s hard to avoid for some use cases.

I've written a decent-sized project backed by Tokio/async Rust and when I'm in pure async code, it's honestly fine and I have few complaints (just... gimme stable trait async fns). I don't think it's worth dropping; async Rust has its place and is continuing to be iterated on.

What needs to happen is what's happening right now: people who write in Rust need to be louder about the fact that crates should be sync first, async second. And/or (my personal irrational opinion) the language needs to get over it and adopt Tokio into Rust proper.

If this proves difficult - the ability to be sync/async via feature flags - then this is something that the language needs to solve before other features IMO (I will admit I'm saying this without checking to see if it's being worked on, though).


It's usually easier to write the async version first and then write a sync wrapper. See for example the `postgres` crate.


Right, I get that.

I'm saying that the sync version shouldn't be "run tokio blocking in the background", because while you might be giving them an out on async infection in theory, you're still making them bring in the entire dependency tree and you're still creating more background resources than is necessary.


The `postgres` crate launches a "current thread" tokio runtime which doesn't really have much overhead in terms of background resources. I agree about dependencies though.

"async infection" is kinda interesting when you think about it. To use `postgres` as an example, talking to a database is asynchronous whether you like it or not. To use blocking APIs to access it is just to allow something (typically the OS) to pretend that it isn't.

In general I've found the experience of writing a program async from the beginning to be very good in Rust, and in the very rare cases (in my field of work) where I have some genuine blocking to do, Tokio provides good tools for that (`tokio::task::spawn_blocking` and `tokio::task::block_in_place`). I can imagine that if you were writing a largely CPU-bound program, async would feel more like an "infection".


I agree to a large extend. However its not that viral if you're the app developer. Tokio ( and iirc most other executors ) have a block_on and a spawn_blocking to jump between 'worlds'.

If all you're doing is something like "get 10 http request and concat them to stdout" then doing it with Rust async + a block_on call is pretty straight forward.


If Rust does decide to continue with async, they should really look into investing in improved docs and education around it. When I was looking at it a few years ago (maybe it has improved since then), I really did feel stupid for not getting the ideas/conventions/reasoning. If it was that off-putting to me, as someone with some experience, I can imagine it is very unapproachable to complete newcomers. It just felt like I was looking at something that shipped way too early.


It's happening, see https://github.com/rust-lang/wg-async . Everyone is aware that current async support in Rust is very much at "MVP" status.


Async is new enough that the function and trait docs are getting better all the time. There was a time it was really ugly to use async, and I still find it a bit crufty but it's a lot more approachable for someone new to Rust.


The async chapter in Rust for Rustaceans was a great introduction. It leaves something to be desired because you walk away with it without knowing how to run any async code (it doesn't mention any runtimes, for instance), but it explains the concepts really well.


> because any code outside of async doing I/O is not good to use inside of async

This is (somewhat) similar in all programming languages and it is called coloured function problem:

https://news.ycombinator.com/item?id=8984648


The way you solve that problem is either 1) just use threads, which is fine for almost every program with the notable exception of webservers (and even for webservers an aversion to threads is often a premature optimization), or 2) make everything async at the language level by transparently intercepting all I/O, which is acceptable for a managed language (so, most languages) but undesirable for a language with bare-metal aspirations like Rust (we're writing a bare-metal hypervisor-ish thing, and we're using Rust because very few languages are acceptable for such a constrained use case (we're not using async for it, but hilariously we could be, since Rust's stackless approach is suitable even for systems too constrained to have threads in the first place)).


There's been some discussion among the Rust language developers about a mechanism to let functions be "generic over asynchronicity", to help alleviate all the duplication. But it's still in the very earliest design stage, and might never happen


Do you have a link to a proposal or the discussion?



There’s a common problem of software design where people believe they need to put their logic and their IO together, and not only is that not true, but it screws up testing, scalability, and feature evolution when you do.

A language that punishes you for not keeping your IO separate? Well, that isn’t necessarily a bad thing. Often when I find people complaining about how hard something is, I find out they’re trying to ice skate uphill, and if they backed up a second and took some advice they wouldn’t be struggling so much.


Microsoft tried to educate developers with best practices by making WinRT async only and it backfired, despite the tooling available in .NET Native and C++/CX for it.

Similarly the way AsyncTask or GCD worked on mobile platforms before other alternatives came into place. Android even went as far as starting to kill processes that were doing network accesses on the UI thread.

All because most will take the easy path if there is an opportunity to do so.


If you don’t mix logic with I/O then why do you even need async-await as a language feature? I’m not disagreeing with you, but the whole point of async-await was to be able to mix asynchronous operations into your code without having to turn your logic inside out.

But if you isolate your I/O from your logic, then what benefit do you get from async-await?

I guess it can still make the “I/O logic” code more readable…


IO callbacks don’t compose. That’s why everyone is switching to async await (that, and faddishness). This problem is amplified by mixing business logic and IO, so the motivation is in the wrong place and continues through the refactor because we’re solving only half the problem.

Most of the interesting business logic is collating data from three or four places, and fetching that data sequentially is high latency. So you can do that part asynchronously and then present the input to sequential (pure) code that derives the answer. That pure code is very easy to unit test.

The “problem” is now I have three kinds of code - IO, logic, and orchestration - and people intuit that this is more complicated than the bookkeeping they do and externalization onto others by mashing all three concerns together in one file, and then duplicate it with elaborate fakes in their tests. Which they are hesitant to write or others are hesitant to maintain, leading to low value, high effort tests.


> And worse, async in Rust is viral

So just like in Java, C#, Python, ...

In other words asynchrony is viral.


I feel like I should respond here.

Yes. The async mechanism is similar to Java, C# and Python.

However, all of those languages you listed are JIT’d languages with automatic memory management.

In Rust, the mantra is very much “zero (runtime) cost abstraction” much like C++, and unlike Java, unlike C#, unlike JavaScript, unlike Python, unlike Ruby, etc. And being a language that deals with performance critical, possibly latency sensitive applications, running on potentially very strapped embedded devices, Rust libraries very much do try to provide you with the absolute best case performance.

With JS, everything being async is completely reasonable because control flow always returns to the event loop anyways. With JS, Java and C#, the runtime performance is plenty fast to do some harder compute tasks, but most of the compute is spent in large abstractions and the applications are still largely I/O bound. With Python, all of that applies except the runtime performance is a fair bit worse, too. At the bottom of all of these languages is a runtime written in low level code and often times third-party modules using C++.

Rust is different. Firstly, Rust is the language at the bottom.

Given all of that, almost any library has to support sync and async for ideal characteristics; there’s no good way to use sync libraries in async and visa versa. But because of the differences Rust has to other higher level language, it’s much harder to accomplish this without thoroughly duplicating code.

While it might never matter if you insist on never writing stuff you’d consider library code, it’s still a design issue, it still impacts the ecosystem, and indirectly, it still will impact you.

I hold that I believe the ecosystem is already strained to try to make fully idiomatic, zero-compromises libraries, and the additional pressure of async bends the story until it breaks. It’s true that Rust’s design for async is not terribly unique, but I feel it is in a uniquely poor position, for unfortunate reasons that are not necessarily the fault of the Rust developers or designers.


> However, all of those languages you listed are JIT’d languages with automatic memory management.

Granted. However, if you mix and match sync with async code in Java, C#. You're going to have a very bad time™. At runtime. Article https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...

Predates stable Rust. Rust didn't somehow add virality to async. It was viral from the start.

And using Rust async with Arc/Box/Pin/etc. works more or less like in GC-land.

> I hold that I believe the ecosystem is already strained

Strained? Not really. Async imo is a bit overblown. Most libs I am contributing don't use async at all.

My opinion is that async was rushed. Pin needed for Async was a well-spring of unsoundness issues, probably still is. Async fn in traits is missing. GATs, etc.

Features that would remove some issues mentioned in the article are being worked on. But then people would be it's 2022 and no async in Rust, joke lang amirite.

Damned if you do, damned if you don't.


Yes, async is viral in those other languages; but the solution is usually, just use async everywhere. That’s not the solution in Rust. It simply won’t work for the same reason that Rust isn’t GC’d.

> Features that would remove some issues mentioned in the article are being worked on. But then people would be it's 2022 and no async in Rust, joke lang amirite.

To be clear, I hate programming language development driven by memes. I don’t think that was the case with Rust async; I think Rust async was released because it was hotly anticipated. It was definitely, at least in my opinion, the reason why Go implemented generics (at least ahead of other potentially more useful changes…)

And yes, it is true: they can technically fix many of these pain points. But I flatly disagree with your notion that the ecosystem is not having difficulty with this, and I also don’t think there is good reason to be optimistic about the timelines for fixing some of these harder problems.

On the ecosystem. I could point to numerous examples, though I really don’t want to single any out. The NATS client library for Rust is an example where I found myself surprised: outwardly, everything looks like it’s in good shape, but internally, it is using Unblock to mash sync code into an async world, which apparently uses a thread pool limited to 500 threads. Now I don’t know how many libraries inappropriately use this strategy, but the fact that it exists and has found use in decently popular production software is not a good sign. Again, I don’t want to start crapping on a bunch of projects, so I’m leaving it at this.

On optimism for fixing the problem:

I think that Rust has made immense progress on resolving issues that have come up and building features around painpoints. However, when it comes to issues around memory management, pinning, etc. there’s been a lot of nearly-intractable issues: technically, they can be solved, but they aren’t being solved. Today, in safe rust, it is impossible to allocate a structure larger than the stack. Placement new is always just around the bend. Rust genuinely delivers on many of its promises W.R.T. safety, so it is immensely disappointing to me how often I’ve run into this problem. I know structures that are bigger than the stack may seem niche, but the stack really isn’t that big, and as far as I know there’s not even a guaranteed size, so if you test on Linux your app may very well work just fine, but then you run it on Windows and it all explodes. To me this is a worst case scenario of failure cases… and I think this is the easiest and most basic conundrum out of all of them. That doesn’t mean it’s easy to fix, of course, but still.

Maybe what they’ve released now is better than nothing, but I do believe the ecosystem suffers for all of the added complexity and duplication. Writing good Rust library code is not easy.


> just use async everywhere. That’s not the solution in Rust. It simply won’t work for the same reason that Rust isn’t GC’d.

At this point, I have to say RC is a primitive form of GC and it would mostly work. Not high performance but it would work.

> To be clear, I hate programming language development driven by memes. I don’t think that was the case with Rust async

First, most programming is driven by fads. Second, my point was that if Rust waited more people would pan it for not having async.

Rust async was "rushed". It took years, but we still didn't get smooth experience out of it. Many pieces were missing.

It was rushed because many people wanted it, many developers were sick of having it in limbo, etc.

> Today, in safe rust, it is impossible to allocate a structure larger than the stack. Placement new is always just around the bend.

I don't follow Rust development too detailed nowadays, but if you are experiencing hardships, have you considered providing support to it? Either via money or time donations?

I understand complaining that some closed source company is prohibiting you from doing something, but this is an OSS project.

https://www.youtube.com/watch?v=9E_rtUgUF58


Honest question: do you really need concurrency to solve problems?


Today's world even power constrained devices like phones have many cores. Laptops and desktops are getting more cores just about every year. Even chromebooks are heading towards 8 cores last I heard.

Take for instance wanting to backup a directory to a remote server and/or cloud. Sure you could use scp, single threaded would be fine, read a file, hand it over to scp, which does encryption and transport.

However you'd waste quite a bit of network bandwidth and disk space. Ideally you want incremental, so you need a database for metadata. You could add compression as well. But you likely want deduplication so if you have a system image for a virtual machine and 1% of it changes you can upload 1%. You also likely want to detect corruption if you upload a backup, then restore and the file has changed.

There are single threaded programs that do these kind of things, but they tend to be very slow, some well known backup clients that promise a flat rate for infinite backups often use the approach to reduce the amount of data it's practical to backup.

So ideally you have multiple threads, one for scanning the filesystem, one for talking to a database of metadata, another for checksumming plaintext, encrypting, and checksumming the encrypted blob, and yet another for managing a queue of blobs to upload.

Without concurrency even simple things like uploading backups can become painfully slow.


Sure... The most obvious case is running an internet service, but if you're asking if I've ever done something where there's either a strong CPU-bound or latency constraints, the answer is "yes."


Rust cannot copy what Go does without compromising on various language design goals. What Go does is good, but what’s good for Go isn’t always what’s good for Rust. That goes the other way too :)

Rust did try to have something closer to Go before 1.0, but it led to so many issues it was removed. Those issues aren’t a problem for Go.


Here are a couple of examples of the changes that were made in the two or three years before Rust 1.0 if anyone wants to read more:

https://github.com/rust-lang/rfcs/blob/master/text/0230-remo...

https://pcwalton.github.io/2013/06/02/removing-garbage-colle...


Go has a runtime and a GC, and importantly can implement an event loop outside of anything reached by tracing execution from your code's entry point. Rust's philosophy lead them to make the runtime something you bring in with a library import and start manually. Bundling Tokio with every binary would not be their way, although something like it may one day wind up in the standard library.


Does Tokio have a concurrency model similar to Go's?


No, since rust async is stackless. There is a stackful coroutine implementation for Rust: https://github.com/Xudong-Huang/may


I'd like to have some clarification on the following claims:

  - Rust async is a mess
  - Python async is a mess
What does "mess" means here?

I have been writing Rust code for a few months, and Python code for more than a decade. I have to admit that I do not know the internals of the Python's async model, but as a "user", I think that trio (and asyncio with 3.10 / 3.11 is getting better) is really great.

The ability to call `trio.run` / `asyncio.run` anywhere to start an async loop does not "contaminate" the async/await keywords up to the entrypoint. Which is great.

In Python, calling an async function returns a coroutine object (that can be awaited). In Javascript it returns a Promise. In Rust it returns a Future.

From the "user" point of view, it's all the same. And I don't know many developers who are interested in how it works under the hood, because (like in math/physics/science in general) it's very useful to sit on the shoulders of others.

If I understand correctly, Rust gives you the tools, and the "shoulders" belong to the library developers (like tokio). Which is fine to me.

So, I really do not understand what "mess" means, what it refers to, and what I (as a "user") can do about it. Also, if the internals change, how will this impact my code?


trio and stdlib has different primitives for sleeping/locking/garhering/etc. And you can't change loop implementation by simple ioloop_of_this_year.run(async_main) in the main().


This is nitpicking, I never said that.

There is still no explanation for what "mess" means, but I don't think it means having multiple competitors providing an implementation.

The fact that there are multiple async Python libraries means that the internals are generic enough to allow that. From what I understand of Rust, there could be another "tokio" as well. And they would not necessarily be compatible either.

This is like saying "You can't use a Django model with SQLAlchemy".


My path to Rust async sanity was using lots of Rc. Though at times when I'm using lots of Rc, I question why I'm not just using a GC language.

I really like Go as a language and you can approximate a lot of its development style by just using channels similarly in your own code. I frequently employ crossbeam channels to that effect.


"Using lots of Rc" is what we call Java Disease elsewhere. Usually it indicates trying to sidestep architecture problems instead of solving them.

Often you Don't Care, and anything that works well enough lets you move on to the next thing. As long as you and people around you agree on the goal and tradeoffs, it is OK.


> Though at times when I'm using lots of Rc, I question why I'm not just using a GC language.

This seems like the wrong way to look at it?

Focusing on the spots in your code where you’re using RC and concluding you ought to be using GC misses the fact that all of the spots in your app where you’re getting along fine without GC would suddenly be forced to bear the costs of GC.

And RC isn’t a poor man’s GC; Apple concluded that Automated Reference Counting was a significant memory and CPU win over running a GC. Unless you’re littering RCs literally everywhere, you’re probably not doing anything much worse than ARC does in Swift (in fact you’re probably doing better if you don’t do it anywhere the compiler didn’t force it, because Swift has less lifetime info and is forced to be more defensive) – you’re more than likely coming out ahead versus what your runtime costs of a GC would be.

(But if you’re not concerned about runtime memory or CPU cycle costs at all, then sure, Rust is probably at the wrong level of abstraction for your problem)


> Apple concluded that Automated Reference Counting was a significant memory and CPU win over running a GC

I really don’t think it is true in the general case. Of course Apple will claim that, having betted on it with Swift, and also it may be true in their general niche (mobile devices with not much RAM). But if we are in a server setting with plenty of RAM available, tracing GCs of e.g. the JVM are quite green on the CPU (which is another important factor in this setting), and they provide probably the best throughput (as GC can happen on another thread concurrently)


I was about to say. You could avoid a ton of ‘Rc’s by just using channels instead.


To a certain extent, it's just enforcing what you have to do for correctness. If you have concurrency (even without parallelism), and objects are shared across contexts, you have to keep track of when objects are in use or not. The other option is enforcing exclusive access (which is just a way of maintaining a refcount of 1)

At least Rust makes it easier with an Rc/Arc type! Can't tell you how many refcounting bugs I've dealt with in C since you have to remember to increment and decrement the count manually.


> Python async/await (which is also a mess)

This! There seems to be a move to (somewhat) "async everything" in Python. But it just results in bad developer UX, it's verbose and unneeded 95% of the time.

I wish Gevent had been adopted as the starting point for an official way to do "none threaded" parallelization.


Lack of 'async' in Go has been a great design choice (aka, an absolute blessing).

People complain a lot about Go's missing features, and they did end up adding generics, but their conservative approach I think has been a net benefit for Go.

Rejecting language feature is something that's hard to do, and it takes a lot of experience, and I dare say wisdom to stick to it.


It's the other way around, Rust tried Go's approach and rejected it. The original incarnation of Rust was heavily inspired by Limbo, which was the language Pike developed prior to Go. As of 2011, you could describe Rust as a slightly lower-level Go, it even had (a poor stub implementation of) a GC. Rust's Go-style green thread runtime was removed in 2014 because it complicated the C interop story too much (which is a tradeoff Go is happy to make).

It's not a matter of minimalism or simplicity, it's a matter of different priorities. Rust's stackless async support is suitable for systems like microcontrollers, and the design reflects that.


> Rust's stackless async support is suitable for systems like microcontrollers, and the design reflects that.

This is the argument you hear, but how does it play out in practice? I don't know of any async runtimes that don't use Arcs, you can't use the async trait crate (boxed futures=allocations) and you don't have the escape hatches needed (Arcs) when you get too deep in type-hell. To this day, I haven't heard any embedded folks praise async Rust. I'm by no means an embedded expert, but if I were to do embedded I'd likely go with a plain old event loop to be safe.

Getting zero-cost (static only) stackless async was a huge flex, and I'm very impressed with the proof of concept. I'm just wondering whether it actually targeted the intended demographic.


I work professionally in embedded rust, and we don’t use async. That’s because we’re aggressively static; there’s no dynamic allocation in the system at all. This isn’t inherently an issue with async on bare metal, but it doesn’t really buy us anything, so we’re not doing it.

That said, it is very cool, and does work well, if you want to do it. I think for many projects it would make sense. For our requirements, synchronicity works, and so we’re sticking with it, but folks on my team use it for other things, just not in the specific context we’re in.

The key is that nothing in async rust requires dynamic allocation. This is in opposition to C++‘a async stuff, which can in theory optimize away the allocations that are required, but you’re relying on the optimizer to do that. Our particular environment allows us to be 100% static, all the time, and so we’re pursuing that route as far as it can go.


You really can get quite far by having everything be statically allocated, placing reasonable constraints on input sizes/rates, and making use of ring buffers.

I've only worked on embedded development using C++, but I didn't really encounter any issues writing (admittedly, somewhat simple) device drivers with no heap whatsoever.


I recently did an embedded rust project. Gave up on rust `async` and rolled my own asynchronous runtime on top of "synchronous" rust. It was much nicer than worrying about the type system. Since it's an embedded project with no OS, it's not hard to get peak performance from a very simple polling async runtime.

The thing that makes async runtimes complicated is handling multithreading in an intelligent way, and that's not something that worries most embedded engineers.


> stackless async support is suitable for systems like microcontrollers, and the design reflects that.

Tangent: How feasible would it be to have a language where the required stack space for all function invocations is statically known at compile time, just as the sizes of Rust futures are statically known? I guess this would be even harder, because with async functions, the compiler only has to store the state of the function in a statically sized structure when it reaches an await point. But I'm guessing that running out of stack is a common problem when developing for microcontrollers.


You can predetermine function stack sizes as long as you don't do anything resembling C's "alloca" (Rust doesn't support alloca directly, but it supports dynamically-sized types in some contexts which would stymie this). But from there you need to use that information to figure out the maximum stack usage of the program, which means exhaustively determining all possible execution paths, which can be a hard problem (you also need to forbid recursion, even mutual recursion, which languages generally don't try to do (something something y-combinator)).


I may be very wrong here, but the JVM byte code’s method structure explicitly has a stack size and local array max size property, both calculated at compile time. Are they using some heuristic here?


I'd bet that the stack size you refer to doesn't include callees (i.e., the stack used by procedures called by the method in question).


Oh you are right, Java has two stacks, an operational and a method/function stack. Only the former is calculated ahead of time (and the latter is configurable at runtime).


The problem is, Go uses a concurrency model (stackful coroutines/fibers) that was all the rage in the 90s (to the point where the Windows APIs have vestigial support for it) and then got abandoned by everyone except them for reasons that are still applicable.

Here's a paper on the history and flaws that was written to argue against adding them to C++:

"Fibers under the magnifying glass" by Gor Nishanov

https://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p13...

TL;DR: They don't play nicely with FFI and that's a big part of why Go is its own little semi-closed ecosystem... the polar opposite of Rust, where things like PyO3, Helix, Neon, cbindgen, etc. are a huge competitive advantage.


The problem is essentially that nobody ever evaluates how good their code is, they evaluate how much better it could be.

Code that has an Arc<Mutex<>> around every single type in Rust could be made a lot better. Code that does the same in Java could not, because Java doesn't have the ability to make types not wrapped in Arc<Mutex<>>. If you wrote perfect code in Java, it would be equivalent to Arc<Mutex<>> code in Rust - which is not hard to write at all, presenting minimal annoyance. Lest I be lambasted for comparing to a JIT language, that's also how everyone writes C++ these days, and for the same reasons.

But that's not how anyone looks at code. They see the ability to unbox types. They see the ability to borrow instead of moving. They see the ability to modify stuff in place instead of making it immutable and then deep-copying it. In most cases the code to do so is shorter and easier. In some other cases, it's longer and more difficult. And people exclusively look at the latter cases, the fact that it's difficult to further improve past what they previously thought of as perfection, and declare Rust to be Hard™, and go back to Java where they can attain perfect code, even if it's worse than the Rust equivalent.

Like nerd-sniping, except you do it to yourself.


> even if it's worse than the Rust equivalent.

If the Java version does the same thing, but with less typing and unnecessary fluff, then it is indeed better.


No its not.

The quality of a abstraction dictates how clear you are able to think about things.

'Less typing and unnecessary fluff' is the difference between writing '100' in Arabic numerals or writing 'C' in roman numerals.


If we're being pedantic then the Rust version is not an abstraction at all. It explicitly states how exactly the object is de-allocated and how it is accessed.

In the Java version, which is definitely not just an Arc<Mutex> but something more opaque and powerful, you let the runtime handle and optimize these things for you, because you really don't want to know. It would only get in the way of the essence of your program, because your program is decidedly not about expressing these things. Well at least the Arc, the Mutex part is not necessarily much better.

So it's not '100' vs 'C' but more like '100 apples' vs 'enough apples'. Both have their upsides and downsides. It certainly is a good thing that both expressions exist.


I like your '100 apples' vs 'enough apples' because it highlights a fundamental choice. The first is a statement that stands alone, whereas the second is a statement requires knowledge of the target/problem you are referencing.

My guess is that in the long run the first is more successful. It doesn't require consensus about a target/problem. However, there are enough places who couldn't care less because everybody knows what enough apples are.


Not everyone values this the same and this is where a lot of preference wars start in PL communities. Some people enjoy keeping the problem space small and having opinionated decisions at the cost of limited expressiveness. Others value expressiveness maximally. This [1] article goes into a lot of the tradeoffs involved with expressivity and cognitive load (with the background of how being a PL enthusiast tends to color one's thoughts).

[1]: https://scattered-thoughts.net/writing/things-unlearned/


It isn’t, though. The point is that you *can* write it without thr Arc<Mutex<>> and gain significant speed without it. Even if your first iteration started with it.


To add to this point, even if you season your Rust code liberally with Arc<Mutex<>>, the libraries you use probably don't, and you'll still get most of the performance benefits.


And Java is much faster than some hypothetical Arc<Mutex> only Rust, as (atomic) RC is quite slow.


C++ wrapped in Arc<Mutex<T>> is not at all how people write C++ nowadays. There are some (poor) code cases where people slap shared_ptr on everything, but even then I've never seen them also also mutex on everything. That's just terrible practice.


Yes. In the C++ world we call this Java Disease.


Not too familiar with C++, but what is "what people write c++ nowadays" that deals with this?


I couldn't agree more! I often tell the team something similar: if you struggle with ownership or getting some code to work, you can always just clone and get the first draft working. You'll still do better than the equivalent in Python or the likes.


I am the author of the original post. Unfortunately, before publishing anything, it's very hard to predict all possible misinterpretations of my text.

> I really wish the author clearly pointed out that they write the article from a point of view of a library author trying to come up with generic and flexible APIs.

Most commentators viewed the text from the perspective of application programming. You are more close to true: I am a library author and the dispatcher example was concerned with the problems of library maintainers. However, I wrote this post mainly to talk about _language design_.

Rust is ill-suited for generic `async` programming, because when you enter `async`, you observe that many other language features suddenly break down: references, closures, type system, to name a few. From the perspective of language design, this manifests a failure to design an orthogonal language. I wanted to convey this observation in my post.

Additionally, how we write libraries in a given language reveals its true potential, since libraries have to deal with the most generic code, and therefore require more expressive features from language designers. This also affects mundane application programmers though: the more elegant libraries you have, the more easily you can write your application code. Example: language's inexpressiveness doesn't allow you to have a generic runtime interface and change Tokio to something else in one line of code, as we do for loggers.

One gentleman also outlined a more comprehensive list of the `async` failures in Rust [1]. This pretty much sums up all the bad things you have to deal with in generic `async` code.

UPD: I added an explanation section [2] to my original post. Thank you for your feedback, this is very appreciated.

[1] https://www.reddit.com/r/rust/comments/v3cktw/comment/ib0mp4...

[2] https://hirrolot.github.io/posts/rust-is-hard-or-the-misery-...


> From the perspective of language design, this manifests a failure to design an orthogonal language. I wanted to convey this observation in my post.

I wouldn't say it's a "failure". It's an incremental design. This stuff is known to language maintainers and it's being worked on as far as I know. I understand your point of view, but I felt like it would be good to point out that it should be viewed only in a very specific context.

> Additionally, how we write libraries in a given language reveals its true potential

Yes and no. Rust has many flaws in this context and yet I think it's still one of the best languages out there. Can it be better? Sure, and I hope it will be. Is it good enough for most of its users? Yeah, I think so.

> One gentleman also outlined a more comprehensive list of the `async` failures in Rust [1]. This pretty much sums up all the bad things you have to deal with in generic `async` code.

I really hate this kind of comments. Saying that async was "rushed" is an insult to all of the people that put so much time and effort into releasing the future. It wasn't rushed, it took years to release it. All of these issues are well known and many people are working on improving the situation and comments like this are not only not constructive - they're actively harmful to the development of the language.

To be clear: I don't mind listing things you find frustrating, it's fine. I just don't like doing it in this kind of unconstructive way that basically just burns out language maintainers.

I really hope the issues listed there can be resolved in time, but if I had a choice between having async in its current form vs waiting for an ideal release in 10 years, I would vote for releasing it even sooner. Again, it's not ideal, it has lots of problems, but I wrote very successful async web services and there are countless companies that did so too, so I'd say it's good enough.


> I wouldn't say it's a "failure". It's an incremental design.

I'd say all this is why you don't usually try and bolt on async (and anything doing multithreading, async, parallel, or distribution) after the fact. It has to be in the language from the very beginning.


The bits of Rust that feel "bolted on" were known to Rust's developers prior to the v1.0 freeze. You can go back through the mailing lists and find talk about things like higher-kinded types and guaranteed tail call optimization and all sorts of other things.

...the problem is that doing this sort of stuff in an eagerly evaluated native-compiled imperative language with no GC and an emphasis on C interop is an area of active research.

Rust is literally pushing the envelope with things like its take on async/await.


Yea the original post reads like "you cannot get rusts' async to do anything useful" and then there were hundreds of trolls jumping in "the language is unreadable" train. "oh yeah, no one can actually use rust". Uh okay... That's why I've been using it in production for two years ok...

Async is hard, that said it really does work just fine.


My intuition is that async is fundamentally the wrong abstraction. First, you want to get rid of function coloring and make coordination itself explicit. Then you build an abstraction over that, where you can declare or infer whether an operation is commutative or associative and generate/select scheduler logic from that. Right?


That is a valid way to design things, as long as it doesn’t clash with other goals. It’s not clear that this is possible given all of the other constraints involved that Rust is attempting to fit together. Doesn’t mean it’s impossible, but the possibility is an open question.

This is partially because some of it is subjective! For example, the whole idea that “function coloring” is inherently bad is not a given in a language like Rust. Languages that want to reach down into the lower levels often make costs fairly explicit. Async and sync functions are significantly different, and so some may argue that in a Rust context, this is a good thing, not a bad one. It’s the same idea with values vs references: in many languages, the difference is papered over, not shown to the end user. But in Rust, it is, and this does lead to some ceremony if you want to call a function that takes one with an argument that’s the other. But nobody is arguing that Rust should totally remove this distinction (other than the fact that references are themselves values but that’s not really relevant here…) due to some sort of two colored functions. However, some do want this for mutable vs immutable references.

Tl;dr it’s not that simple, in many contexts.


This is basically what zig does "async", though it's not coordination so much as "context switching control flow points" that are made explicit. It's very low to the metal and if you think about what the machine is "actually doing" it makes a ton of sense, and it's kind of a "shift" in the way of thinking about things in the same way that pointers are. I rather dislike how it's called "async" though. I feel like "reentrant" or "detached" makes more sense, because that's what it feels like to me but if you write your own scheduler you basically have what everyone else calls async.

Iirc rust takes an async function and turns it into a abstract state machine, and I feel like this is a surprisingly high level concept for a systems programming language


I'll say. That's how C# handles async too


C# is in a highly managed VM, so I can't see how it would be the same as zig. The alternative (high level) way to have "nice" async, which, if c# is similar to zig I think they made "the wrong choice" is to layer async over green threads/cooperative multithreading + message passing/channels.

Elixir's Task.async function is I think very well done, leveraging the isolation of erlang's VM and doing a whole ton of things "just right" (a future is process id + unique reference, monitoring the task when you await, e.g)


I was talking just about a sync functions being compiled to a state machine. It’s been awhile but I’m pretty sure C# does the same thing


Got it. Thanks for clarifying. That seems like a not- unreasonable choice for a HLL like c#, though I'm still baffled they didn't go with something like Task.async


Pet theory, as someone who falls into the "Loves Rust; avoids Async and generics" alluded to in the beginning of this article, the one it's replying to, and comments on the latter's thread here:

Is the Async crew mostly writing web servers and other things that operate using TCP and HTTP? Lower level (eg IP, Eth, network hardware drivers) isn't well supported by rust libs. Nor is higher - we have Flask analogs, but no Django analogs.

As Async ingresses in Embedded Rust, I seek answers to "is this worth the viral qualities, and API rift?".

For the adjacent question re generics by default, I ask "Is the flexibility and type checking worth the API complexity, and documentation dead-ends?"

Does anyone here use Async Rust in domains outside those sections of network and web programming?

I've found rust to be a great fit as a cleaner, more explicit C alternative.


The problem is that async in Rust is, as other people have put it, viral.

Example: I have some stuff in my hobby application that is executing in a WASM virtual machine. That's not async, simple and synchronous.

But I have two other parts: one that talks to FoundationDB, and another that receives websocket connections. Both use Tokio & Async.

Now I'm in a pickle every time I want to hold or pass around some state in my WASM pieces, because the async stuff ends up pushing its constraints all the way down. Want some piece of mutability? Some piece that doesn't Send or Copy? Good luck.

There's ways around it all, but it complicates the design. The 'async' pieces at the front end up propagating all the way down.

It's hard to explain fully without you being in my code, but suffice it to say, I agree with others: async in Rust is half-baked. That should be evident enough simply from the fact that you can't even yet put async functions in traits.


> you can't even yet put async functions in traits.

I find the compiler reminding me constantly that I can't. I don't worry much about the viral nature of async, but this annoys me so much.


> Is the Async crew mostly writing web servers and other things that operate using TCP and HTTP? Lower level (eg IP, Eth, network hardware drivers) isn't well supported by rust libs. Nor is higher - we have Flask analogs, but no Django analogs.

Something I have never understood is why people want to write web-apps in Rust. I love the language, but honestly, a managed language (like js/ruby/go) is always going to be better suited to the world of web-apps.

I feel like this imposes an undue burden on Rust to do everything for everyone which has to break down somewhere


For me, I’m currently running a Rust webapp and a Python webapp on a server; both are of similar complexities, and I’m probably a bit better at Python. I keep on having to fix the Python app, because of both OS upgrades and problems with the implementation. The Rust app has been working without any issues for the last three years.

For a relatively simple web server, where I don’t need to collaborate with anyone, I’m choosing Rust.


I've got a similar thing. I wrote a bespoke time-series database in Rust a few years ago, and it has had exactly one issue since I stood it up in production, and that was due to pessimistic filesystem access patterns, rather than the language. This thing is handling hundreds of thousands of inserts per second, and it's even threaded.

Given that I've been programming professionally for over a decade in Python, Perl, Ruby, C, C++, Javascript, Java, and Rust, I'll pick Rust absolutely any time that I want something running that I won't get called at 3 AM to fix. It probably took me 5 times as long to write it as if I did it in Go or Python, but I guarantee it's saved me 10 times as much time I would have otherwise spent triaging, debugging, and running disaster recovery.


The answer to this question is often one of a few different things: reliability (“we didn’t touch our service for 18 months after deployment and it never ran into issues”), low resource usage (“we decommissioned X servers which saves us $Y/year”), and high performance are common responses.


Isn't Go pretty close to optimal here, though? C/Rust may use somewhat fewer resources, but when you look at Go's efficiency over, say, scripted languages like Python, Ruby, or JavaScript, further optimization seems like it's gilding the lily.


It depends on what you mean by “optimal.” For many cases, sure, it’s not an issue, but Go has a big runtime that needs to exist. And its expectations mean that C FFI is not zero overhead, which is why you see so many pure Go packages, but isn’t appropriate in many of Rust’s use cases, which require zero overhead.


I agree - however, once you cross the border into C linkage/FFI, you’re losing both Go’s and Rust’s safety features. That seems like a wash to me. Besides, people are doing a splendid job writing pure Go libraries. It helps that Go has some fantastic observability tooling to help performance-minded engineers minimize unnecessary heap allocations.

Finally, the Go runtime just isn’t that big. A few megabytes, I think. That’s well below the noise floor for most server environments, and it’s far smaller than, say, a Python or Java runtime.


Yes, you're not wrong. I have gotten some threads crossed; I forgot this one is about the server side context. Yes, these sorts of things are often less of an issue than in other contexts.

Sometimes it still matters though. Just really depends on what you're doing.


For most use cases, I agree.

But Rust is a better option when you also need one or more of the following:

a) tight control over allocations

b) deterministic performance

c) strong data race protection

d) zero overhead C interop (e.g., processing audio using ffmpeg)


FYSA I upvoted this for teaching me that cool and useful expression!


>Something I have never understood is why people want to write web-apps in Rust.

The vast, vast majority of web apps are the same CRUD endpoints and background job queue processing. I would label Rust as mature enough in this regard; I've written quite a few of these and am not left wanting in any regard (short of a clone of Django's automagic admin, but few things get close to this anyway...).

When you accept the fact that you're taking in some data, shoving it into the DB, and returning a response, the rest of the Rust ecosystem becomes pretty damn nice - deployment is an absolute breeze compared to some of the other ecosystems (Ruby/Python). I don't need to mess with Docker, I don't need to deal with broken package managers, I can just replace a binary and call restart.

It's amazing.


Common things webapps use that Rust frameworks don't provide:

  - High-level DB migrations
  - Auth
  - Email
  - Admin
  - Templating
You get these immediately with eg Django, but not in Rust. I'm not trying to be contrarian; I'm confused, and have never heard an answer about this point.


No worries, I don't think you're being contrarian - I think projects that properly do this stuff have trouble rising above the micro-framework-style projects.

https://github.com/secretkeysio/jelly-actix-web-starter

I put all of this together some time ago, explicitly modeled after Django. This repo isn't up to date with actix-web 4 as I've been too busy with other (paying) work, but it absolutely exists. I've used this for services in production with no issues. I really do need to get time to finish and push the updates though.

Mine isn't even the first, I'm pretty sure there are others working in this space. Just needs the right people pushing it.


Oh wow! Very appropriate. Looking forward to seeing this fleshed out when you get more time. Will give it a shot on my next project.


People can be wary of these bolts-included/on-rails solutions because there is typically hell to be paid as soon as you venture slightly off of the rails. If not, they also tend to have an overabundance of features; leading to slowness or a fertile ground for vulnerabilities.

> High-level DB migrations

Having used Datadude[1] I can say for certain that there are few approaches worse than migrations. The problem is that, once you are stuck with a turn-key framework, you are completely stuck with their approach (which is usually horribly archaic).

Unbundling all of these things might require more work up-front, but can result in significantly improved productivity in the long run.

[1]: https://www.sqlshack.com/working-with-database-projects/


> People can be wary of these bolts-included/on-rails solutions because there is typically hell to be paid as soon as you venture slightly off of the rails

This is so very often the case, and you often can't tell in advance before you use the framework or library or whatever. You just have to use it, and get to a certain point of maturity in your project before you discover these limits.



Better suited in what way? I've written HTTP APIs in all of the above - plus Python and PHP - and choose Rust for new ones 99% of the time.

Just recently I had a need for a fairly simple webapp. Whipped up the back end in Rust in, like, an hour; only had to bolt on a couple tests for the single complex bit of business logic, because I have a high degree of confidence that the compiler has me covered for everything else.

Now it sits there quietly using 4.9 MiB of physical memory and 1.5 millicores on two of my servers, primarily serving a k8s healthcheck every couple seconds.


If you accept the divide between back-end and front-end (and the need to have separate developers for each), this is fine. But I, for one, prefer full-stack server-side frameworks such as Rails, Django, and Phoenix, where the server side does HTML generation, HTML form processing (with CSRF protection), authentication with session cookies, etc. Bonus points if, like Phoenix or ASP.NET Core, the framework has integrated support for real-time applications using WebSockets. AFAIK, Rust doesn't have anything quite this comprehensive, though such a framework could certainly be developed.


Nope, as far as I know there's no serverside HTML generation crate. Granted, I haven't looked, but I feel like it would have come up on /r/Rust sometime.

Everything else you mentioned is out there, although I didn't love the websocket experience the one time I needed them for something.


Most rust web frameworks are server side, and generate HTML, so I’m not sure what you were looking at, but it absolutely exists. Most rust web stuff is server side generated.


Sure, obviously you can generate strings containing HTML and send those. I'm imagining something like the serverside rendering you get with e.g. React. That said, I don't know what specifically the previous poster was looking for.


I'm a bit confused, what's the difference? SSR React also ends up "generating strings containing HTML and sending those."


I don't really know what you mean by serverside HTML generation crate. HTML is just HTML. What I usually need is some templating system, and that can be used to produce HTML, SQL, or whatever else I want.

Use askama or tera, for example. Here's an example of using askama with axum: https://github.com/djc/askama/tree/main/askama_axum

If you mean something more significant, like what an ORM/query builder is to SQL, then I haven't really had a need for such a thing myself. It's far more likely to get in my way when I stray past common scenarios.


I’ve written quite a lot of rust GRPC web services. The reason being that we wrote a lot of propriety derivatives library code in Rust that we needed to Interface with. We could have wrapped this in python, but to be honest, it just wouldn’t have been worth the effort.

I ended up writing something that faced external clients in rust, that had to do traditional web app things, and it was okay. The pros were that you could write idiomatic interfaces to things like auth that wouldn’t look out of place in a Django app. The cons however, was that writing these Interfaces took some non trivial rust knowledge, especially since we did a lot of async rust.


I've been experimenting with Rust for web apps instead of Go, so I haven't used it in anger yet, and have started working on a blog post to get my thoughts straight. My experiments are for web apps that focus on server side rendering with just a little javascript through HTMX and AlpineJS.

The main thing I'm finding an issue with Rust so far are things that I could find packages for in Go. The main one is having an oidc helper library that can take various oidc services (github, twitter, etc), and provide me a standardised 'account'.

Apart from that, I'm finding the experience nicer overall. Examples:

* I have better/easier control over errors in ways that don't clutter up the readability of my handlers. I can easily define global rules about how errors from different systems (database, IO, etc) should be returned to the client.

* Compile time checks for things that would fail at runtime in Go, and therefore require more extensive tests base to catch when refactoring --sqlx and askama are examples enabling this (askama is particularly nice for this -- more work to get things to the stage of compiling)

* When it finally compiles, it usually just works, due to aforementioned fewer ways for failure at runtime

* Can express/encode some functionality much more succinctly than Go

* Macros for auto-generating code (though I haven't needed this in Rust yet), where with Go I'd make a small external tool for auto generating code. I use generated code a lot in Go, and even with the new 1.18 release, I see that continuing.

With more time for the ecosystem to grow, I can see Rust being just as useful. Apart from the ecosystem and the nice fact that you don't need .await's in Go, I can see Rust being really good for this sort of thing.


I don’t find the overhead of Rust to bad for web apps. Saying that I used to write web apps using EJB’s, JSP’s, Apache Struts although have also used NodeJS, various Jamstacks and PHP frameworks, Rails and a bunch of others extensively.

At the web level the biggest pain I have is the dev workflow isn’t as nice as the Jamstacks with hot reloading so you can see visual changes immediately in the browser. If doing backend for front end this pain goes away if front end is some NodeJs / php or other app.

Why is my preference now Rust? It’s fast and low on system resources. It means I can run my apps at a fraction of the cost compared to other languages. I can package my app in a Docker container that’s single digit MB’s. It’s not that to low level to be an inconvenience and high enough level to be reasonably productive. Being strongly typed and being able to find bugs with the compiler also helps.


100% agree. It drives me nuts when people use the wrong tool for the job.


Are you confident it’s the wrong tool though? Curious have you tried web apps with framework such as Actix ?


If I would write it in C/C++, I would instead write it in Rust. Otherwise I would pick something more appropriate for the job. As far as performance, Java and Rust are neck and neck getting their asses kicked by Javascript (for web apps)!

https://www.techempower.com/benchmarks/#section=data-r20&hw=...


And yet real-world Rust projects consistently have significantly lower memory footprints than real-world Java projects (with people often commenting on the memory savings they observed from porting away from Java, even just to Node.js) as well as being free of some of Java's most common runtime exceptions.


It doesn't matter for webapps. At all. Further, they don't understand the defaults the JVM uses for maximum throughput. It uses most of your memory on purpose as it expects it is the only process running on your server. Generally you can set the max memory usage to be much lower and get basically the same performance. But who thinks anymore you know?


Funny. It seems to matter enough to me that I'm making plans to rewrite projects I can't host out of GitHub pages in Rust to optimize how much VPS I have to pay for.


I'm suspicious this occurs when there are too few tools in a given box. See also: NodeJS.


"Is the flexibility and type checking worth the API complexity, and documentation dead-ends?"

What do you mean?


I was implicitly referring to [Typestates](http://cliffle.com/blog/rust-typestate/) as an example; the details will depend on which generic patterns are used. I like the advantage they provide by catching incorrectly-configured hardware, but in practice, I don't think they're worth the application-side code complexity and learning curve vice using simpler APIs.


The big question is if one can still get a job by doing the above. If the wider Rust ecosystem will be infiltrated by async and heavy generic code one would be condemned to use Rust for personal projects.


How do you manage to avoid generics while writing Rust?


You can't avoid traits and generics entirely, but you can get by with far less than the norm (which is simpler, has some powerful code-reading advantages, and some expressiveness drawbacks). Personally I keep using Vec<T> and other hashmaps, I generally prefer match over .value_or().map(...) functional-style Option/Iterator chaining (though my opinion is unwelcome in some "oh-so-accepting" Rust spaces), prefer type methods over trait methods (which you can't even call unless you `use` the trait into scope), etc. Unfortunately when building generic data structures, you sometimes need complex lifetime and trait Send/Sync/Sized bounds (but I try to switch approaches and sometimes write duplicated code, whenever a particular abstraction approach starts requiring complex HRTBs and such).


You can't avoid using generics, but you can avoid writing generic data structures if the standard library is sufficient and your application won't benefit from de-duplicating code shared between similar structs.


It depends on the use case. The short and simple answer is avoid libs that rely heavily on them, and use structs and enums. This gets into the area of application code vs libraries. A simple example, for say, a struct used to interact with a bus on a MCU is to use a `I2c` struct, instead of `I2c<Output<OpenDrain<PA5<Af5>>>, Output<OpenDrain<PA6<AF5>>>>` etc. God help you if the library that uses the latter doesn't document it using examples, because the auto-generated Rust docs won't help.


Recent and related:

Rust Is Hard, Or: The Misery of Mainstream Programming - https://news.ycombinator.com/item?id=31601040 - June 2022 (655 comments)


If you read the article this post actually links that article and explains that it was written to address it...


But it doesn't link to the HN discussion, which is what dang, HN's moderator, is helpfully doing.


Yes, that's why I posted the above. When an article is responding to an article that was recently discussed on HN (or to the HN discussion itself), such a link is particularly relevant.


I write a decent amount of Rust and I find it productive, but I can see how it might be easy to get nerd-sniped trying to get rid of every last allocation. There's no shame in a Box/Arc. Remember that almost every language puts almost everything on the heap. Just allocate. It'll be fine. Really.

I set a rule for myself that I'll spend up to one minute trying to save an allocation. Beyond that it's not worth getting sidetracked.


My rules of thumb for Rust, that for the most part keep me out of allocation puzzles, are:

- Never combine two things that can be valid for different amounts of time into one struct. For example: A file struct should not contain both information about how it is formatted, which is true forever and can be used for many files, and a file handle, which could be invalidated by the OS at any time. Breaking this rule will fill your code with Box/Arc as you try to imitate classical OOP.

- Don't be afraid to frequently pass contextual information to functions; you don't need to put everything that will remain the same between two calls into `self`. For example, every function that works with the file can take the file handle and the format information as separate arguments. Trying to DRY function arguments by combining data with different lifetimes into a single struct and then hiding that argument in the `self` parameter might feel like simplification, but in Rust it triggers the above problem.

- Functions that call functions that take mutable references as output locations should do the same, unless they have to allocate for another reason. This rule of thumb will tend to push allocation as far outside of loops as possible.

With these in hand, I almost never need to box or reference count anything. If you fail to heed the fact that Rust is not really an OOP language, your entire program will start to look like the hairy parts of C libraries that interface with the Python interpreter.


> If you fail to heed the fact that Rust is not really an OOP language, your entire program will start to look like the hairy parts of C libraries that interface with the Python interpreter.

Any tips for those of us who see PyO3 as one of Rust's biggest killer apps?

(Honest question. I hate how unmaintainable Python is but I don't know of any better equivalent to PyQt/PySide's memory-safe QWidget bindings or the RAD-friendly ORM migrations in Django ORM or SQLAlchemy+Alembic.)


I imagine it would go like writing a good C library for Python, where the PyResult<> wrappers disappear as you move deeper into your code and away from the interface. Hopefully working with Python objects on the outside won't require using references everywhere on the inside.


Ahh, so more or less what I'm doing. Design the Rust code as if it's going to be a generic C library with bindings for multiple languages and then write a PyO3 binding layer that just clones however much is necessary to convert until it's proven that more optimization is needed.


> I set a rule for myself that I'll spend up to one minute trying to save an allocation. Beyond that it's not worth getting sidetracked.

My rule is that I'll do what is the easiest, unless it's an inner loop that runs all the time, where I'll try to make it fast.

In rust, usually the easiest is to not allocate or copy data. When it's easier to allocate or copy, I don't do a cost-benefit analysis at all, I just do it.


> I can see how it might be easy to get nerd-sniped trying to get rid of every last allocation

Honestly, this is a really good way to put it, and that one minute rule sounds like a pretty good rule (ignoring cases where performance requirements led to profiling, which pointed you at some specific piece that you need to optimize, obviously)


I am like a moth irresistibly attracted by the far away light of zero allocation. I can almost reach it. Just one more little lifetime annotation. One more...


You can return structs to an allocation pool ring buffer by writing a custom implementation of the Drop trait. If you do that, almost anything can be zero-allocation. In Zig you can control the allocator as a first-class citizen and, I think, are meant to do things like that.


Hopefully allocators will soon be available in stable Rust as well - then you can optionally specify an Allocator for a given allocation.


This is similar to Go’s sync.Pool to reuse buffers and prevent allocations when you want to optimize things, right?


Yes, this is similar.

In Rust you control yourself the allocation on the stack or the heap. So before reaching out to a memory pool/arena, it's too tempting trying to put everything on the stack for "maximum performance".


Yea, i definitely agree with this author more than the last. Also, as both a writer of apps and libraries, i agree libraries pose more opportunity to drag yourself deeply into generic relationships and hyper optimizations.

Strangely i haven't had many of the issues that the previous poster was discussing, though. My issues are usually trying to work around the lack of GATs, lack of trait aliasing, etc. But i use `async_trait` so maybe i'm sidestepping many of the issues from the previous post. /shrug


Absolutely agreed. You can get so caught up in trying to make it perfect that you don't ship something.

https://raw.githubusercontent.com/luser/keep-calm-and-call-c...


> Remember that almost every language puts almost everything on the heap

True, but then those languages are heavily optimized for that scenario. This is why Rust written like Java often performs worse than Java (and optimized Rust).


>I set a rule for myself that I'll spend up to one minute trying to save an allocation. Beyond that it's not worth getting sidetracked.

This! Though I modified my rule to mostly being "I'm punting this to the weekend as a fun exercise", since I often find stupid enjoyment in seeing if it's possible.

Programming in Rust can often be a very good exercise in telling yourself to stop trying to be clever.


so something I wonder, in a typical language you can easily pass by reference between components and threads. Avoiding any allocations.

In rust the unspoken rule seems to be to allocate, allocate, allocate - unless you are writing a special purpose library etc. Which makes me wonder, is rust actually faster than a GC'd language when doing heavy async work?


I think a lot of Rust users would argue they don’t even care much about performance. They just enjoy all of the correctness guarantees the compiler can enforce, as well as how ergonomic the language can feel. Being able to deploy a single static binary, and having an easy to use build tool and package manager are significant bonuses as well. Rust makes really hard problems easier when you know the compiler has your back.

Rust gives you the tools to write very high performance code, but it doesn’t have to be about that.

I’ve had to point out to people quite a few times that garbage collectors can improve performance… especially compared to naive implementations of manual memory management. GCs are not just a tool for lazy programmers. GCs can make allocation incredibly fast, and you get to defer cleanup work to another thread(s), which means less work in the critical path. Removing work from the critical path is how you make software faster. Every tool has tradeoffs, and GCs are a tool. GCs often use more memory as a tradeoff.

I like Rust well enough, but I do wish we had a language that combined the ergonomics of Rust with the dead simple concurrency model of Go/Erlang. I haven’t tried it, but Luantic looks promising: https://github.com/lunatic-solutions/lunatic

As it is, we’re fortunate to have quite a few great languages and platforms these days.


> I think a lot of Rust users would argue they don’t even care much about performance. They just enjoy all of the correctness guarantees the compiler can enforce

Aren't these correctness guarantees only for performance related factors though? (allocation/memory and concurrency). The rest of your program would be just as correct in any other language.


No. A ton of languages don’t support proper Sum Types, and Rust’s emphasis on errors-as-values helps you think about error handling, instead of only thinking about the happy path. Rust also doesn’t do implicit type coercion and a host of other things that can cause correctness issues. Rust gives you the tools to express more of what you’re doing to the compiler than a lot of languages, which lets the compiler help you more.

It’s natural that a lot of programs have some form of concurrency, so that’s an extremely common thing for Rust to help with, but it’s not the only thing.


Not really.

For me, the number-one Rust feature I love is that, by baking monadic optionality and error return in from the beginning (Option<T> and Result<T, E>), I can trust that, unless an author abuses panic! (in which case I never trust their code again), I can see all a function's return paths in its type signature. (Without having to choke down a pure functional language like Haskell with that currying-based function call syntax that I can never get used to.)

The runners-up are how fast Rust starts compared to Python or Java or similar when writing a CLI tool and how nice PyO3 makes safely writing libraries or backends for tools that need to be in Python for some reason like "nothing but PyQt, PySide and possibly QtJambi offers memory-safe QWidget bindings... and I've never found a Java app that didn't feel laggy and sluggish on X11".

See also https://cliffle.com/blog/rust-typestate/


interesting! The main reasons I like rust are

- Can compile to anything, you can use one language to write Cuda Kernels, backend services, distributed processing jobs, WASM apps, and native apps.

- Performance, comparable to better than C.

I like fast languages as it avoids headaches that come from slow languages. If rust wasn't fast, I'd probably just stick to a polyglot language portfolio and pass it by.

EDIT: I also just clicked through to lunatic, that does look very promising!


You should also check out Gleam! https://gleam.run/

It's almost like Rust and Elixir had a baby :D

As for the rest of the comment: totally. Almost none of the code I write in Rust needs C-like performance and I still choose Rust for it.


> in a typical language you can easily pass by reference between components and threads.

and that's when you usually get data races ;)

> In rust the unspoken rule seems to be to allocate, allocate, allocate - unless you are writing a special purpose library etc. Which makes me wonder, is rust actually faster than a GC'd language when doing heavy async work?

I think it's a bit more nuanced. `Arc` is still a "smart pointer", so while it's not a straight Rust reference, it acts as a pointer. Yes, it allocates and it needs to do a reference count, but the overhead is very small. So while in practice it is not "zero cost", it's usually negligible.


This isn’t quite accurate. It’s generally really easy in Rust to pass things by reference, and even inner async fns, this is easy. The issue with async and Futures, is that sometimes you need to capture the future and then pass that to something else to execute. In that context, shared references are hard, and just clone, or arc box like mentioned.


Fwiw i rarely allocate around these "issues" and i use all async. I think your comment could be tweaked to say:

> In rust the unspoken rule seems to be to allocate, allocate, allocate when you run into a lifetime issue

Lifetimes work fine with async, but some types of lifetimes can be problematic, for sure.


> in a typical language you can easily pass by reference between ... threads

Yeah. And that is almost always an error on those languages too.


It's often not. Many high- performance libraries use fine- grained locking within logical units (eg locking buckets within a hash table in the fast path rather than locking the table), which almost necessitates sharing references.


So, you keep the hash table read-only on a static context, and only mutates some internal references?

In rust you will have to declare it exactly like that. That's not really an example of passing mutable references between threads.


Largely complaints of Rust seem to boil down to the programmer needing to describe object lifetime information in code.

We can, as the original post did, show approaches that are overwhelmingly difficult in Rust because of this but trivial in Go. Alternative approaches, as this post shows, can be relatively straightforward.

In a similar vein, a Python programmer might complain about having to explicitly describe object type information in Go code. One supposes they could show approaches that are overwhelmingly difficult in Go, but trivial in Python.

Python, Go and Rust programs all do have types and object lifetimes. It is just that mistakes in type are not found until runtime in Python, and likewise mistakes in lifetime are not found until runtime in Python and Go.

Personally, after years of Python I came to value describing types in code, and after years of Go I came to value describing lifetimes in code too.


I’ve always felt the “if you use Arc” why not just use GC is a weak response. The benefit of rust is that you only pay the cost if you need it. I’m not saying “yay it’s all easy” I found concurrency frustrating in rust because it refused to let me do things that I “knew” we’re safe :)

The always use Arc model is actually what swift and objc fundamentally do. Everything’s lifetime has to be threadsafe, so the refcount itself must be threadsafe, and so any refchurn is atomic. For a single thread as I understand it modern CPUs handle uncontended churn without a real perf hit.

But I was writing a raytracer in swift, and once I made it multithreaded the refcount cost on my non mutating objects became a massive perf cost. It was super frustrating, and is fundamentally what would happen if you took the “Arc everything” approach. But you don’t have to, and this get perf where it’s safe and possible.


One of the downside is that the programmer now needs to use different way to allocate and create objects because it's just too complicated to make it work with the borrow checker.


>But I was writing a raytracer in swift, and once I made it multithreaded the refcount cost on my non mutating objects became a massive perf cost.

Then access them through `unowned` references?


Because I was not skilled enough in swift land to know of such. I'll read up and adopt and see what happens, and try to remember to update here :D

[edit: Just discovered the last thing I was trying to do was separate out some core data structures into packages. It turns out that if the pop() function on your priority heap gets left out of line, that becomes vastly more expensive than is reasonable. So fixing may take a wee bit long than expected]


Priority heap in a raytracer? :thinking_face:


Photon mapping collection :)


I'm currently writing an async rust application. I think the biggest thing to avoid coding yourself into a corner with async rust is to prefer transferring ownership over using references when possible. Channels are a great way of doing this, and likewise communicating with sync code


Great post/points. Really enjoyed your rebuttal.

Your version of the dispatcher really shows a simple and intuitive way to code without any explicit lifetimes or zero-alloc-shenanigans.


I can get behind the general recommendations outlined in this article (with the caveat that they're only to be used if you don't need every last drop of performance, you're not writing a library, and you find the current async situation difficult) except for the complete cop-out of implementing each handler routine as not just a function (which could at least be nested/local) but as a completely separate type implementing a trait.

That's fine if all your transforms are strictly defined, often reused, and you're just choosing between them but if many of your transforms are just one-offs then that's an insane amount of boilerplate and a very clunky approach. It's also antithesis to the OP's claimed "just get things done" approach since you'll always be second-guessing whether something should be a separate transform type or if it should be extending an existing one, etc.


Async does have to be hard, sometimes, at least right now. Iterators, closures, selects, and more are IMHO hard, or absent, or not intuitive. I know these are being worked on-- thank you to the language developers.


I think it depends on what you call hard. Things you listed usually make things unergonomic, not necessarily hard. You can still do a lot of stuff with async Rust, but it often requires a lot more boilerplate and compromises.

Granted, it can be hard, but I wrote a lot of async code, including async streams, traits, saving `Future`s for later execution etc and usually you don't need anywhere near as much complexity as was presented in the first post.


Async is a cancer that has spread from single thread languages like JS and Python. Its the wrong solution for multi-thread languages like Rust/Java/C++/Go.


> So I'll start with a note for all the people intimidated by the techniques the author is trying to use in the post: when writing Rust code you almost never use this kind of stuff

Never write async code? Or expect a library will always cover every use case where async might be needed?


Is there much support for debugging async Rust functions? I'm not able to get breakpoints to trigger in async Rust functions when using lldb.

Am I missing something? This seems like a REALLY broken experience.


Given the amount of bikeshedding that went into the design of async Rust, it's mind-boggling how they ended up with this clusterfuck.


Tokio is awesome and very easy to use imho


It’s interesting that to criticize rust without being downvoted to death, one must first say how wonderful and superior to c++ it is.


Please don't take HN threads into tedious flamewars, and definitely not tedious meta flamewars.

We detached this subthread from https://news.ycombinator.com/item?id=31613361.


You mean that acknowledging the strengths of a technology and adding constructive criticism is not downvoted while claiming that "it sucks" without any argumentation is?

Who would have thought...


No. It means that people are still touchy and insecure about Rust and its merits, and (rightfully) worried that being obliged to stand just on its merits it will fizzle.

Rust still has a very great deal of evolution to get through before it is ready to compete on an even playing field. It has very little time in which to achieve this, the competition is not sitting still, and the numbers are strongly against it

Rust could still become a mainstream language if it changes enough not to repel the majority of professionals who try it out on production-level problems. A clear majority of existing users would object to the needed changes.


GC-less languages would have to break backward compatibility to implement lifetime/ownership management.

Rust's goals do not fit every single problem out there. As the author said in their articles, Rust is not a general purpose language.

Rust makes you care about many subjects that are solved in an opiniated way by higher-level languages. It wants you to be explicit about those problematics, it wants you to carefully consider the implications and make your own decisions. Rust made me realize that nothing is trivial: even `printf()` can fail.

Certainly, this is not a good fit for the "don't be afraid to break things" philosophy.

But when you are in a field where you care about that, and you need the safety, Rust is actually, to my knowledge, the only tool that can help you.

Rust is complex, because programming is complex. Other languages are simpler, because the decisions have been made for you, and you can only accept it or use another language.

For C and C++, you need extra tools to get to the same level of confidence about your code (just look at the SQLite toolchain/test suite).

You can criticize Rust, just like you can criticize anything. You just have to be constructive about it.

You'll never see the following comment being upvoted:

  Rust syntax is rotten garbage, it's unreadable.
Why? Because it's subjective, it is definitely not constructive.

But the following comment might be upvoted:

  Rust is a complex language that is difficult to approach for a newbie.
Because it acknowledges the problems Rust is trying to solve.

None of those hypothetical comments are praising Rust.


Notice how I didn’t say it sucks.


"There are only two kinds of languages: the ones people complain about and the ones nobody uses."




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: