[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)