[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)
 
web link (itsallaboutthebit.com)
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 and Result), 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> 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>. If you wrote perfect code in Java, it
| would be equivalent to Arc> 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 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> 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>>>` 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 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)