[HN Gopher] Async Rust doesn't have to be hard
       ___________________________________________________________________
        
       Async Rust doesn't have to be hard
        
       Author : drogus
       Score  : 156 points
       Date   : 2022-06-03 17:45 UTC (5 hours ago)
        
 (HTM) web link (itsallaboutthebit.com)
 (TXT) w3m dump (itsallaboutthebit.com)
        
       | ntoskrnl wrote:
       | 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.
        
         | marcosdumay wrote:
         | > 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.
        
         | [deleted]
        
         | bombela wrote:
         | 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...
        
           | whatshisface wrote:
           | 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.
        
         | lumost wrote:
         | 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?
        
           | marcosdumay wrote:
           | > in a typical language you can easily pass by reference
           | between ... threads
           | 
           | Yeah. And that is almost always an error on those languages
           | too.
        
             | [deleted]
        
             | pclmulqdq wrote:
             | 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.
        
               | marcosdumay wrote:
               | 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.
        
             | [deleted]
        
           | bluejekyll wrote:
           | 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.
        
           | coder543 wrote:
           | 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.
        
             | ptato wrote:
             | > 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.
        
               | ssokolow wrote:
               | 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/
        
               | coder543 wrote:
               | 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.
        
             | lumost wrote:
             | 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!
        
             | drogus wrote:
             | 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.
        
           | lijogdfljk wrote:
           | 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.
        
           | drogus wrote:
           | > 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.
        
         | lijogdfljk wrote:
         | 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
        
         | JoshTriplett wrote:
         | 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...
        
         | whatshisface wrote:
         | 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.
        
           | ssokolow wrote:
           | > 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.)
        
             | whatshisface wrote:
             | 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.
        
               | ssokolow wrote:
               | 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.
        
         | mcronce wrote:
         | > 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)
        
         | substation13 wrote:
         | > 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).
        
       | RazeLighter777 wrote:
       | 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
        
       | dang wrote:
       | Recent and related:
       | 
       |  _Rust Is Hard, Or: The Misery of Mainstream Programming_ -
       | https://news.ycombinator.com/item?id=31601040 - June 2022 (655
       | comments)
        
         | crabbygrabby wrote:
         | If you read the article this post actually links that article
         | and explains that it was written to address it...
        
       | olliej wrote:
       | 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.
        
       | ComputerGuru wrote:
       | 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.
        
       | wolfspaw wrote:
       | 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.
        
       | pie_flavor wrote:
       | 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(tm), 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.
        
         | jstimpfle wrote:
         | > 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.
        
           | athrowaway3z wrote:
           | 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.
        
             | dgb23 wrote:
             | 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.
        
             | Karrot_Kream wrote:
             | 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/
        
           | felipellrocha wrote:
           | 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.
        
       | Hirrolot wrote:
       | 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-...
        
         | dgb23 wrote:
         | 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?
        
           | steveklabnik wrote:
           | 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.
        
         | drogus wrote:
         | > 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.
        
           | crabbygrabby wrote:
           | 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.
        
           | dmitriid wrote:
           | > 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.
        
             | ssokolow wrote:
             | 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.
        
       | jph wrote:
       | 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.
        
         | drogus wrote:
         | 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.
        
       | jmartin2683 wrote:
       | Tokio is awesome and very easy to use imho
        
       | tus666 wrote:
       | > 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?
        
       | daenz wrote:
       | 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
        
         | whatshisface wrote:
         | 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.
        
           | erikpukinskis wrote:
           | Does Tokio have a concurrency model similar to Go's?
        
             | oandrew wrote:
             | No, since rust async is stackless. There is a stackful
             | coroutine implementation for Rust:
             | https://github.com/Xudong-Huang/may
        
         | steveklabnik wrote:
         | 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.
        
           | ssokolow wrote:
           | 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...
        
         | Karrot_Kream wrote:
         | 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.
        
           | felipellrocha wrote:
           | I was about to say. You could avoid _a ton_ of 'Rc's by just
           | using channels instead.
        
         | samwillis wrote:
         | > 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.
        
         | kosherhurricane wrote:
         | 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.
        
         | jchw wrote:
         | 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.
        
           | daenz wrote:
           | 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.
        
             | Karrot_Kream wrote:
             | 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.
        
             | ollien wrote:
             | 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.
        
           | berkut wrote:
           | 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.
        
             | the_gipsy wrote:
             | 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.
        
             | SuperFluffy wrote:
             | 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.
        
               | ibraheemdev wrote:
               | 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.
        
               | armchairhacker wrote:
               | 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.
        
               | Matthias247 wrote:
               | 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.
        
               | berkut wrote:
               | 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).
        
           | miohtama wrote:
           | > 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
        
           | athrowaway3z wrote:
           | 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.
        
         | ssokolow wrote:
         | 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.
        
         | qsdf38100 wrote:
         | It's interesting that to criticize rust without being downvoted
         | to death, one must first say how wonderful and superior to c++
         | it is.
        
           | linkdd wrote:
           | 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...
        
           | qsdf38100 wrote:
           | See?
        
             | qsdf38100 wrote:
             | I love it keep them coming!
        
         | linkdd wrote:
         | 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?
        
       | the__alchemist wrote:
       | 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.
        
         | xavxav wrote:
         | > 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_
        
           | gbear605 wrote:
           | 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.
        
           | sonthonax wrote:
           | 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.
        
           | spullara wrote:
           | 100% agree. It drives me nuts when people use the wrong tool
           | for the job.
        
             | the__alchemist wrote:
             | I'm suspicious this occurs when there are too few tools in
             | a given box. See also: NodeJS.
        
           | mcronce wrote:
           | 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 one of my servers.
        
           | steveklabnik wrote:
           | 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.
        
         | bool3max wrote:
         | How do you manage to avoid generics while writing Rust?
        
           | whatshisface wrote:
           | 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.
        
           | the__alchemist wrote:
           | 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.
        
           | nyanpasu64 wrote:
           | 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).
        
         | tester756 wrote:
         | "Is the flexibility and type checking worth the API complexity,
         | and documentation dead-ends?"
         | 
         | What do you mean?
        
           | the__alchemist wrote:
           | 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.
        
         | cmrdporcupine wrote:
         | 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.
        
       | aaaaaaaaaaab wrote:
       | Given the amount of bikeshedding that went into the design of
       | async Rust, it's mind-boggling how they ended up with this
       | clusterfuck.
        
       ___________________________________________________________________
       (page generated 2022-06-03 23:00 UTC)