13 ms·
The bane of my existence: Supporting both async and sync code in Rust
- fireynis 3y agoI may be the outlier but sync and async are both tools in the tool belt and ignoring one or the other is mostly silly?
- Yujf 3y agoWhat does this have to do with the article? The problem is for libraries: they want to support both sync and async use cases, without having to think about think about it, so that the user of the library can decide what is right for them.
- topspin 3y agoThis story is about not ignoring either sync or async... It's great that the work finally led to both being supported. The cynic in me wonders if it's also async runtime agnostic, and not just Tokio. If that's possible.
- omginternets 3y agoMy friend, did you jump to conclusions here? ;)
- fireynis 3y agoI meant to more distill it like, any time you are making a choice between async or sync, you are making the wrong choice!
- bruce343434 3y agoRewrite it in Erlang
- biomcgary 3y agoFrom the article: >The problem - Well, the thing is that features in Rust must be additive: >“enabling a feature should not disable functionality, and it should usually be >safe to enable any combination of features”. Isn't the real problem that async Rust colors all the functions red?
- andrewstuart 3y agoI don’t know how different it is from Rust, but in javascript supporting async and sync is smooth as a Swiss watch. In Python it takes more thinking and structure because async isn’t built in deeply like it is with javascript. Even so, async versus sync doesn’t feel like a nightmare in either. Having said that, it depends entirely on how much experience you have with async programming. Async programming is a mind bender that will throw you completely until it becomes natural. Synchronous programming feels natural and logical and async requires a completely different mental model. I can understand why anyone forced to do async would hate it. It’s something you have to want to do.
- sgbeal 3y ago> in javascript supporting async and sync is smooth as a Swiss watch. Ha! Integrating async I/O sources, namely the OPFS API, has been the single biggest development-time hit and runtime performance hit in sqlite.org's JS/WASM build of sqlite. As soon as a single method is async, it cannot be hidden behind a synchronous interface because the async attribute is "viral," requiring the whole API above it to be async (which sqlite is not). We instead have to move all of the async I/O into its own worker and "fake" synchronous access to it using a complex proxy built from SharedArrayBuffer and the Atomics API. It's an abomination but it's the only(?) approach for making async functions behave fully synchronously which doesn't require third-party voodoo like Asyncify. PS: the opposite - hiding sync stuff behind an async interface - is trivial. Hiding async behind a synchronous interface, however, is a tremendous pain in the bottom in JS.
- sgbeal 3y ago> It's an abomination but it's the only(?) approach for making async functions behave fully synchronously which doesn't require third-party voodoo like Asyncify. PS: it's also limited to argument and result types which we can serialize into a byte array. i.e. its utility is more limited than direct use of async APIs is, as those can accept any argument type and resolve to any type of value. That fine for the case of sqlite but it's not a general-purpose solution.
- matt_kantor 3y ago
- gavinhoward 3y agoI hope that someday, we can have a Rust-like language without async. Bonus points if it has the ability for users to define static analysis a la borrow checking.
- andrewstuart 3y agoAsync programming is beautiful. It’s the easiest and most natural way to do multiple things at the same time in single threaded code. Async makes things possible that are hard or impossible to do with synch programming. It’s a real game changer for python especially. Cant comment on rust, hopefully its implementation is smooth.
- nsm 3y agoAsync/await is a _concurrency_ mechanism, and concurrency is certainly naturally useful. However async/await is certainly not the best implementation of concurrency, except in the narrow design space Rust has chosen. If you have a language with a runtime, green threads/fibers/coroutines, along with a choice mechanism is really the way to go. It trades off some performance for much better ergonomics. Go, Java, Racket all use this. It is unfortunate that Python picked async/await.
- WaxProlix 3y agoI mostly agree, but I think you can still just pip install gevent and have greenlets as you always did - just not as language 'primitives'. Personally I've never liked the syntactic sugar on top of function calls very much. If something is a promise or whatever, return me the promise and I can choose what to do with it.
- TwentyPosts 3y agoIs it really though? All I personally care for when it comes to "doing multiple things at the same time" is frankly Rust's scoped threads API: Create N threads, have them perform some computations, then join them at the end of the scope. How is this not more natural than creating various state machines and passing around all sorts of weird highly abstract Future-objects?
- omginternets 3y ago[flagged]
- sigmonsays 3y agohaving jumped from go to rust, I still really appreciate go. Everything is rust is way harder and more complicated than in go. However, with that added complexity you build rock solid software. Many times over i've ran into issues in go where something deep down in a library returns a nil on a pointer and my crap blows up in unexpected ways.
- andrepd 3y ago> Many times over i've ran into issues in go where something deep down in a library returns a nil on a pointer and my crap blows up in unexpected ways. Doesn't that mean precisely that Rust is easier than Go? Finding those bugs must be a nightmare, not to mention the impact of them happening in the first place, in prod... Usually people say X is easier than rust because writing a program in X is easier than writing a program in Rust. When in fact they should be comparing (writing a program + debugging) in X vs (writing a program + debugging) in Rust
- omginternets 3y ago>Doesn't that mean precisely that Rust is easier than Go? I suppose that boils down to whether you find it easier to wrangle Rust-isms or track down runtime bugs/panics. In many cases, I'd rather wrangle Rust-isms. In some cases (and they're not rare), I'd rather wrangle Go's runtime errors. What makes some Rust people insufferable (not you, friend!) is that they think everything is about the language and toolchain, and they lose sight of the tremendous relevance of the application. There's a reason not everybody is switching to Rust, and it's not because they're ignorant or stupid.
- riwsky 3y agoOr maybe… certain problems lend themselves better to some languages than others, even when accounting for debugging, and Rust just isn’t the winner every time?
- bvrmn 3y agoSync/async mismatch is a curse of a modern development. Countless hours are spent to make things compatible in a diverse ecosystem. I maintain a simple abstract middleware caching library for python and it was a trick issue how to support combinations of sync/async app code and sync/async actual caching backends with async introduction in Python. In the end the answer was an automatic runtime AST transformation for all cases. Ugh. The same approach I use in validation library to parse requests for different web frameworks. But it's a specific case and could not be generalized. It's the opposite of fun situation.
- deleted 3y ago[deleted]
- the__alchemist 3y agoI run into this sometimes for `std` rust libraries; generally involving networking. Not directly mentioned in the article, but I prefer to wrap a `block_on` in a thread, and use `std::sync::mpsc`, or as required, to manage state. On embedded, I make my own libraries for everything; the open-source community has gone full-in on either Async, or an typestate/generic API. I don't see this changing any time soon, but maybe later down the road. I feel like the only one who doesn't like either.
- bpye 3y agoDo you have any open source embedded projects? I’m curious of the direction you’ve gone in.
- the__alchemist 3y agoHere's one: https://github.com/David-OConnor/stm32-hal https://github.com/David-OConnor/stm32-hal Device IC code generally ends as files in my firmware directly.
- MrBuddyCasino 3y agoNah same, I find the typestate pattern to be overprotective against bugs that usually get caught during development, and async Rust is just a killjoy overall.
- rufius 3y agoThe time I the most Rust was the couple of years before Rust's async/await settled. I worked on what is now a large code base that processes a lot of data quickly. It was multi-threaded but no async/await. When I tried to start converting things over, it was really painful. I have no idea where it landed, but I remember coming away from it thinking that I'd prefer to just not deal with the async code in its current state. I mostly write Go now and haven't had to think about this. Sounds like it's still painful.
- deleted 3y ago[deleted]
- Laaas 3y agoMake two crates? rspotify-sync, rspotify-async? Couldn't you perhaps make an async runtime that isn't async and just blocks? That would let you keep only the async interface.
- anonymoushn 3y agothat solves the whole problem except the part where the consumer of the library has to deal with async
- hermanradtke 3y agoThe article has a whole section about the problems they ran into with two crates: https://nullderef.com/blog/rust-async-sync/#_duplicating_the_crate https://nullderef.com/blog/rust-async-sync/#_duplicating_the...
- martinky24 3y agoIt's very obvious you did not read the article. This is literally covered.
- xedrac 3y agoWhen writing some IO bound application, async Rust is great. Less great for libraries that want to support both async and sync without having to make an async runtime a dependency if you just want the sync interface. Mutually exclusive features are taboo unfortunately. One thing I really love about Haskell is you can make any function run in a green thread by simply composing it with the 'async' function. There's nothing special about it. This works much better than say Go, because Haskell is immutable.
- XorNot 3y agoI'm not clear what your last sentence has to do with anything else? What does immutability have to do with Async/sync conversions?
- throwaway0665 3y agoyou can fearlessly run code on another thread if you're not worried it's going to cause a data race or mutate anything
- XorNot 3y agoThis is very much not free though. Predicting memory usage in Haskell programs is notoriously tricky (and all the memory copies aren't free either).
- c-cube 3y agoIt's also the case with OCaml, Elixir, clojure, etc. Non lazy languages can also have a rich collection of immutable data structures and have more predictable memory usage than Haskell. On the other hand, Go doesn't have a culture or features that encourage immutability.
- bippihippi1 3y agoit's not free, but it is explicit. it's nice for the code to explicitly define how the memory is being modified. there are copies in java too, you just have to know how the runtime works to know what each line does.
- tooltower 3y agoThe comment threads here discuss async in many different languages: Rust, Go, JavaScript, Python. Can somebody knowledgeable describe how they are subtly different between languages? Why are they painful in some but not in others? Is there already an article that describes this well?
- vlovich123 3y agoThey’re painful in all contexts because of function coloring. They’re slightly less painful in Go and JS because there’s a single opinionated async runtime built in. In Rust they have yet to standardize a bunch of stuff that would remove the pain: async traits in std instead of each runtime having their own, a pluggable interface so that async in code doesn’t have to specify what runtime it’s being built against potentially an effect system to make different effects composable more easily (eg error effects + async effects) without needing to duplicate code to accomplish composition Keyword generics as the current thing being explored instead of an effect system to support composition of effects With these fixes async rust will get less annoying but it’s slow difficult work.
- yawaramin 3y agoThere's no function colouring in Go. Async functions don't have any special colour.
- SkiFire13 3y agoIMO saying there's no function coloring in a language ignores a lot of details. In Go there's no function coloring because there are only async functions. That's why they don't get any special color, they are the only color. In Go you don't get to use sync functions, which creates problems e.g. when you need to use FFI, because the C ABI is the exact opposite and doesn't have function coloring because it only allows you to use sync functions. Zig and Rust's async-generic initiative are a bit different in that they want to allow functions to be both sync and async at the same time. Ultimately there are still colors, but you don't have to choose one of them when you write a function. However IMO there are a lot of non-trivial problems to solve to get to that result. Ultimately Go's approach work well enough, and usually better than other approaches, until you need to do FFI or you need to produce a binary without a runtime (e.g. if you need to program a microcontroller)
- corethree 3y ago[flagged]
- doakes 3y agoWhat's the advantage of async if you immediately call await?
- maleldil 3y agoFrom the library perspective, they're yielding control to the runtime. This means the user can do whatever they want with the future.
- sodality2 3y agoIf you're I/O bound, it allows for easy and efficient utilization of resources by enabling other tasks to run while waiting for I/O. I'll give you a real world example. I wrote some code that listened to a websockets URL from thousands of Reddit posts - specifically, the one that sends new messages on new comments - so I could see a stream of Reddit comments for any given sub. Implemented it using Tungstenite (synchronous) and it created thousands of threads to listen, and used enormous chunks of memory (several GB) for the stack space + memory reading for every single WS stream. Implemented it using Tokio_tungstenite, the async alternative, and it used a handful of MB of memory and barely any CPU to listen to thousands of WS servers.
- doakes 3y agoBut at what point are you calling await? If I were using the author's library, I would call `.some_endpoint(...)` and that would return a `SpotifyResult<String>`, so I'm struggling to understand why `some_endpoint` is async. I could see if two different threads were calling `some_endpoint` then awaiting would allow them to both use resources, but if you're running two threads, doesn't that already accomplish the same thing? I'm pretty naive to concurrency.
- sodality2 3y agoI was calling await on ws_stream.next() - awaiting for there to be a new message. Code: https://gist.github.com/sigaloid/d0e2a7eb42fed8c2397fbf8423999035 https://gist.github.com/sigaloid/d0e2a7eb42fed8c2397fbf84239... In the example you give, yes, it's just sequential and everything relies on a previous thing. But say you are making a spotify frontend - you want to render a list of playlists, the profile info, etc - you call await on all of them and they can complete simultaneously.
- shpx 3y ago> Another possible way to fix this is, as the features docs suggest, creating separate crates. We’d have rspotify-sync and rspotify-async, and users would just pick whichever crate they want as a dependency A nitpick, but please, if you do this for your library, name the sync one just the name and put "-async" in the name of the other one, so "rspotify" and "rspotify-async" instead of "rspotify-sync" and "rspotify-async". This mirrors the way the function definition keywords are "fn" and "async fn" instead of "sync fn" and "async fn". Most simple usecases don't need async and the extra typing and extra reading is annoying.
- skybrian 3y agoYes, a one-letter difference seems easy to miss.
- bbkane 3y agoI think the explicit -sync/-async is nice.
- deleted 3y ago[deleted]
- fshbbdssbbgdd 3y agoSorry to pick on this comment in particular, but it being the top voted on the post seems like a good illustration of bikeshedding.
- darrenBaldwin03 3y agoI like the clear separation of sync -> async in rust but the lack of first-party lang support for async stuff has been really annoying. Its getting much better though - cool to see async traits in recent release
- ndesaulniers 3y agoForget "what color is your function?" "What color is your module?"
- winrid 3y agonimlang solves this with multisync
- SethMLarson 3y agoNice! This is similar to the solution here: https://github.com/python-trio/unasync https://github.com/python-trio/unasync
- anon291 3y agoThe main issue with async/sync differentiation in all languages is that all languages have a blessed category / monad in which they execute, and the async support (or whatever other execution schemes) are 'bolted on'. Haskell makes this clear in its type system, and while the monadic functions are much more generalizable than async/await (which seems to be the one monad every other language wants to implement for some reason), they're also not 'complete'... there are functions in the prelude with obvious monadic equivalents that are not part of the prelude. Nevertheless, even if they were, you cannot interchange them with the blessed 'pure' monad (equivalent to the Identity newtype), which is the Kleisli monad of the Hask category. There has been work to enable generic compilation of all Haskell code to arbitrary categories (see Conal Elliot's compiling with categories) but unfortunately the approach has not caught on. It would actually be an interesting design space to support arbitrary categories as a basic design principle of a novel programming language. Obviously at some level something would need to be the 'special' one that can be compiled to machine code, but if the complexity could be hidden / abstracted that would indeed be interesting.
- Arnavion 3y agoAnother option is to implement your API in a sans-io form. Since k8s-openapi was mentioned (albeit for a different reason), I'll point out that its API gave you a request value that you could send using whatever sync or async HTTP client you wanted to use. It also gave you a corresponding function to parse the response, that you would call with the response bytes your client gave to you. https://github.com/Arnavion/k8s-openapi/blob/v0.19.0/README.md#not-restricted-to-a-single-http-client-implementation-and-works-with-both-synchronous-and-asynchronous-http-clients https://github.com/Arnavion/k8s-openapi/blob/v0.19.0/README.... (Past tense because I removed all the API features from k8s-openapi after that release, for unrelated reasons.)
- deleted 3y ago[deleted]
- eximius 3y ago`block_on` seems like the right balance of code duplication and overhead, imo. It is a somewhat strange compile time jumble of libraries but a compile time overhead isn't so bad.
- junon 3y agoThis is my take, too. Write async and then use block_on. It's worked for us just fine, albeit a little annoying. We made macros to help clean things up.
- eviks 3y ago> Fixing maybe_async and adding _async and _sync suffixes to each endpoint in our library. Can you not add a suffix only to one variant, e.g., "_sync", as far as I understood you just need different names?
- mst 3y agoI use quite a few promise/future based APIs that have a sync version and an async version and the async methods are called get_p or get_f or so, which feels like very little noise when I'm already typing 'await' in front of/after it.
- eviks 3y agoThat's why I thought async should be shorter, and also since it seemed for this lib async is the primary interface
- nyanpasu64 3y agoCan you create separate modules for sync and async user-facing APIs?
- zubairq 3y agoI’ve had the same issue with async and await in JavaScript. These days I just err on the side of simplicity and make everything async
- Too 3y agoThe fear of increasing the binary size sounds like a self-imposed constraint. How bad is it to include both versions in same crate?
- palmfacehn 3y agoThat's a long road to avoid copy/paste and search/replace.
- deleted 3y ago[deleted]
- nullderef 3y agoBut where would the fun be then?
- nullderef 3y agoI'm quite late to the party, but I'm the creator of this article. Let me know if you have any suggestions :)