18 ms·
Why asynchronous Rust doesn't work
- thibran 5y ago> As someone who used to really love Rust, this makes me quite sad. The current async story is still an MVP and I too dislike it. In the months before async, the ecosystem seemed on halt, waiting for async to land on stable Rust. Since then nothing has changed. The ecosystem "degraded" noticeable and has not recovered since. Maybe in future async will be great, but right now I try to avoid it. ... I still love Rust
- darthrupert 5y agoI think your comment hits the bullseye better than many others I've seen around this. It very much looks like the language stopped progressing with async. Seems like they just tried to be everything at once, and reached some sort of a critical mass. It probably didn't help how Mozilla dropped lots of their Rust projects and staff at roughly the same time. It's still a very good C++ replacement. I think its role as a higher level application language is more of an open question.
- pornel 5y agoSorry to spoil your axe-grinding with hard data, but the Rust async ecosystem has exploded since 2019. It is now about 5-6 times larger than it was before async/await landing: https://lib.rs/crates/tokio/rev https://lib.rs/crates/tokio/rev And Rust as a whole keeps growing exponentially: https://lib.rs/stats https://lib.rs/stats
- thibran 5y agoThat's not what I was talking about. Before async we had one ecosystem. Now we have at last three: non-async, Tokio, async-std My point is, the ecosystem experience has degraded since the async MVP is stabel.
- pornel 5y agoYou're conflating programming models with async runtimes, and weirdly attributing them to stabilization of a syntax sugar. The split between blocking and non-blocking code existed in Rust before stabilization of async/await (e.g. futures 0.1 existed 3 years before, and mio is older than even Rust 1.0). The only new thing is async-std, but that's not a stabilized part of Rust. It's just a 3rd party library. It may have been written anyway. It's relatively niche (1/10th of tokio), and you can safely ignore its existence (as far as I can tell every async-std-based crate supports tokio too, or has a direct tokio equivalent.) I was worried that switch from futures 0.1 to std::future would fragment the ecosystem, but fortunately the whole ecosystem moved quickly, and the switch is completely over now.
- ushakov 5y agoofftopic, but i've had yet to see a website with dedicated "Name & pronoun information" page
- davnn 5y agoI think most people are less enthusiastic about their names.
- archseer 5y agoSee https://news.ycombinator.com/item?id=26406989 https://news.ycombinator.com/item?id=26406989 for previous discussion
- shsbdncudx 5y agoThose are brave words around these parts
- vbg 5y agoI’m not an expert by async rust seems to work pretty well to me. I guess I’ll have to become an expert to find my disappointment.
- mrspeaker 5y agoWhich part of the article are you disagreeing with? Just the title?
- jokethrowaway 5y agoThe article complains about how hard it is to pass closures with references - which is a valid criticism; I run into that as well and while you can fix it, it's not as straightforward and as documented as it should be. It doesn't have much to do with async, despite the title. Async works just fine.
- kohlerm 5y agoIn summary asynchronous programming without a GC is a pain in the a*. That's somewhat expected isn't it?
- hmrr 5y agoIt’s a pain with GC as well, coming from a C# background. It’s incredibly easy to write something that intermittently doesn’t work in weird and impossible to debug ways.
- scotty79 5y ago<laughs in JS> Async is bread and butter of JS and you rarely have any larger bits of sync code, so you learn to deal with this. And then being able to just freely pass bits of data accessing code (closures) around is such wonderful feeling that you'll miss it everywhere else you go. I recently had a little excursion into Python and had to invent a very nasty hack to concisely keep short bits of code accessible through some constants.
- eska 5y agoFrontend or backend js? Because in the frontend everything is just scheduled to a single thread, so you only deal with concurrency, not parallelism.
- spoiler 5y agoIt's still concurrency in Node, since there's no threads in JS. There's the `worker_threads` (never used it though) which is like web workers in a browsers, but those work by posting messages to eachother.
- wccrawford 5y ago"Modern" frontend JS is increasingly async. Yes, it's not true async, but it still has all the problems, but the benefit of not locking up the UI is worth the pain. Even old frontend JS was async in that events could be triggered by the user at any time, and in any order, and xhr and image loads requests were async as well.
- kccqzy 5y agoThe article glosses over async Rust and is mostly a rant about how closures are difficult in Rust. Most of the difficulty comes from Rust not having a GC yet wishing to keep track of object lifetimes precisely: a GC'ed language needs no distinction between an ordinary function pointer and a closure that captures the environment. But Rust being a low-level systems language chose not to have a GC. Another popular language also doesn't have GC, makes distinctions between ordinary function pointers and closures, and its closures are unnameable types; that language is C++. But instead of using template for closures everywhere (like the STL), you can optionally use std::function<R(Arg)>, a type-erased closure type, which would in principle be similar to Box<dyn Fn(Arg) -> R> in Rust. But such a type doesn't really mention the lifetimes of whatever it captures, so in practice it doesn't work well in Rust (though std::function works well in C++ because the C++ doesn't care about lifetimes). With that in mind, I see this article as not understanding the goals and tradeoffs of Rust. The author would be happier writing in a higher-level language than Rust.
- scotty79 5y agoFrom my very limited expeirience with Rust I noticed that it becomes way more easy and laid back language when you just skip using references and lifetimes nearly completely and just wrap everything in Rc<>. Then you are getting expeirience of fairly high level language with a lot of very cool constructs and features like exhaustive pattern matching and value types with a lot of auto-derived functionality. Does Rc<> help this much with async as well?
- mrfox321 5y agoIf you are going to wrap everything in Rc, just use kotlin or c-sharp or python?
- 5e92cb50239222b 5y agoAnd suffer a 100 MB runtime? If anything, Rust with refcounts for everything is much closer to Swift.
- Noughmad 5y agoThe author spends the majority of the post saying how synchronous Rust doesn't work. The main problem? Closures have to be passed as traits. And they either don't know about the `f: impl Fn(i32)` syntax or refuse to use it for some reason. Then the last paragraph just say "Oh and asynchronous Rust is even worse."
- loeg 5y agoIn synchronous Rust, closures (largely) don’t need to have asynchronous lifetimes. You really don’t have to think about them all that much.
- ungamedplayer 5y agoGuess I'll just go back to erlang.....
- jsiepkes 5y agoThere is also a actor pattern implementation for Rust: https://actix.rs/ https://actix.rs/ But yeah, the way Erlang embededded the actor pattern in the VM (Beam) and the language itself is great. Tough personally I would have liked it if it was more statically typed. I still need to take a look at Gleam...
- rad_gruchalski 5y agoI love erlang but my problem with it is the app distribution model and runtime dependence. The language is really nice and simple.
- mortemeur 5y agoThere is also Caramel[0]. [0]: https://caramel.run/ https://caramel.run/
- mitchtbaum 5y agohttps://github.com/actix/actix https://github.com/actix/actix https://actix.rs/book/actix/ https://actix.rs/book/actix/
- eska 5y agoDid that, ended up writing more C for performance critical code than I thought.
- lawn 5y agoRust + Erlang/Elixir is a fantastic combo. Rust is safer than C, so there's a smaller risk that it ends up crashing and taking the entire VM with it. Rustler[0][1] makes the integration a breeze. [0]: https://github.com/rusterlium/rustler https://github.com/rusterlium/rustler [1]: https://hexdocs.pm/rustler/readme.html https://hexdocs.pm/rustler/readme.html
- fulafel 5y agoExcellent post. Async ruins everything even in GC languages. It's just making things needlessly hard for programmers in an attempt to save effort from computers. Erlang shows how to do massive scales of slow IO if you really need it (most programs don't).
- jsiepkes 5y ago> It's just making things needlessly hard for programmers in an attempt to save effort from computers. Well actually it is supposed to make concurrency easier for programmers. What would you say is an "easy" way to deal with concurrency? Handle threading, memory visibility issues, locking, etc yourself? Doing it "reactive"? I guess personally I too would say something messaging based such as the actor pattern but not everything is a good fit for that model. Async is intended to be a more generic (ie. fit for everything) and easy way to program. I'm not saying it succeeds in that but the goal wasn't to save the computer the effort.
- masklinn 5y ago> What would you say is an "easy" way to deal with concurrency? The Beam/Erlang/Elixir model. It's a bit more constraining but it's just so clean and reliable, and processes having individual mailboxes makes it much simpler than having to wonder what synchronisation primitives to use, then decide how to share them (globals? parameters? others?). Just get a reference to a process and you can send it messages, simple. If you want replies, send it your address. Also simple. And matches the real world very, very well (at least the real world of a few years back when you'd send letters through the mail, electronic communication channels tend to be more full-duplex).
- jsiepkes 5y ago> The Beam/Erlang/Elixir model. It's a bit more constraining but it's just so clean and reliable [..] I agree. But I think that only works if you embed it in the language like Erlang did. If you make it optional you get libraries which use different models, people still making concurrency mistakes, etc. But for more generic languages then Erlang such as Rust (or Java, C#, etc.) this simply isn't an option. Because the actor pattern is too opinionated to be a fix for all.
- Aissen 5y agoIt was heavily discussed previously. In particular, it triggered this response from Rust contributor withoutboats: https://news.ycombinator.com/item?id=26410487 https://news.ycombinator.com/item?id=26410487 And this blog post from someone who did spend a lot of time working with async rust: https://tomaka.medium.com/a-look-back-at-asynchronous-rust-d54d63934a1c https://tomaka.medium.com/a-look-back-at-asynchronous-rust-d...
- mirekrusin 5y agoWow, thanks, that was burried from me, thanks for surfacing it.
- auggierose 5y agoOh, thanks for that link. Withoutboats is being quite emotional, but it is understandable to be emotional about their brain child. I am tempted from time to time to try Rust, but in the end, I don't think it provides enough benefits for me to consider it. I like high-level, and when I am going low-level, I don't want to be hand-cuffed. It seems with Rust, I am paying all the time just for the ability to go into hand-cuffed low-level mode.
- jdub 5y agoNah, you're really not. Most of the time, particularly if you're not writing libraries, Rust feels like a dialect of Python that wants to help you get things right.
- auggierose 5y agoWell, 90% of what I am doing is writing libraries. I know how to get things right. Does Rust?
- ssokolow 5y agoHonestly, as a consumer of libraries, I much prefer when they're written in Rust (without gratuitous use of `unsafe`) compared to a language which doesn't make you think about these sorts of things as hard.
- DougBTX 5y agoHave to day, mixing async and CPU intensive code (I’m using Rust because it is good at CPU intensive code!) has been a frustrating experience, with lots of noise and baggage associated with spawn_blocking to make things run well.
- selfup 5y agoI have had great luck using rayon for distributing CPU intensive work across available cores. If you are doing I/O I can see why async is preferable but rayon is an excellent library for parallel work. I have found that async or even in Go with goroutines, a large amount of small threads are not faster even if they should be in theory when it comes to CPU intensive work. I use it to generate a 3D universe (at the atomic level) but store the data as enums. Here is the repo: https://github.com/selfup/oxidizy https://github.com/selfup/oxidizy Big help from rayon: 1) https://github.com/selfup/oxidizy/blob/master/crates/unigen/src/builder.rs#L31 https://github.com/selfup/oxidizy/blob/master/crates/unigen/... 2) https://github.com/selfup/oxidizy/blob/master/crates/unigen/src/builder.rs#L64 https://github.com/selfup/oxidizy/blob/master/crates/unigen/... 3) https://github.com/selfup/oxidizy/blob/master/crates/unigen/src/builder.rs#L106 https://github.com/selfup/oxidizy/blob/master/crates/unigen/... Example perf generating an obscene amount of data on a 32GB machine with 16 threads (3800x): Atoms: 110592000 Baryons: 26099712000 Quarks: 78299136000 real 0m12.779s user 0m0.000s sys 0m0.000s
- xondono 5y agoMaybe it’s only my take, but from what I understand, the author wants to easily create closures with mutable references and call them from anywhere, asynchronously? And the complaint is that Rust semantics makes this hard. Well yes, Rust makes it uncomfortable to shoot your own foot, that’s kind of the point.
- uncomputation 5y agoThat was my impression too. Rust “makes it hard” for a reason: it’s not safe. If you work with the type system to prove that it is, you can go about and do your thing but Rust is about guarantees. That’s one great thing about it. You can’t ask it to just not guarantee something.
- api 5y agoThe author wants Go, Java, C#, or another GC’d language.
- valenterry 5y agoWhy would the first example with the database be "shooting your own foot"? I'm not a Rust dev, but in other languages that code makes perfect sense to me.
- xondono 5y agoFor the same reason that keeping a shared excel file in a network disk is a bad idea. The closure can write to database, and after the call, there’s no way to know if it has already written, or even if it’s writing to it right now.
- valenterry 5y agoWell, without further ado that's true. But the callback could of course set a result type somewhere (so, behave similar to a future/promise) and then we could know if/when it is finished and if it was successfully, or if it's still running (or broke without an error). But that's not Rust specific, that's just plain language-independent logic no?
- KingOfCoders 5y agoI think callbacks do not scale with large projects as they make understanding the flow difficult (at least from my experience of multi million LOC code bases based on callbacks that called callbacks). Using callbacks in Rust is not idiomatic, at least I haven't seen such code over the last two years writing Rust, and Rust syntax and semantics with life times is not tailored toward that approach. So the author forces an async style on Rust that it was not build for and then complaints.
- flohofwoe 5y agoI don't have any experience with async Rust (but I stuggled a lot with Rust's closures when I dabbled with Rust a while back so I can at least feel the pain the article tries to convey), but one important reason to not build async-await on top of fibers or threads but instead on code transformation (aka 'compiler magic') is 'weird architectures' like WASM, which doesn't have easy access to threading (locked behind COOP/COEP headers), and where the call stack is inaccessible to code running inside the VM. I didn't quite get the gist of the article though, isn't the whole point of async-await to get rid of passing callback function pointers around? What do closures have to do with async-await in Rust?
- fnord123 5y ago> What do closures have to do with async-await in Rust? Well, space _blocking runs blocking code in an async context. It takes a closure as it's argument and if you're in a mixed async/sync platform you will his this function a lot. To deal with it you end up needing to use move. And to use move you need to clone your pointers to the data and manage all that guff. The alternatives are Arc, deep copies, or use C++ where you will invariably get it wrong and end up with corrupted data and a very bad week (or you don't notice and reply to this comment saying that you do this in C++ and never had a problem)
- Rusky 5y agoI think the article is just... wrong about async and closures. You're right that async Rust simply doesn't deal with them, because (contrary to the article) they're not used in the async compiler transformation.
- gpderetta 5y agoI think you can implement fibers and continuations in WASM by converting the whole program into a giant switch statement and heap allocating frames. There are a few scheme-to-C compilers that do that I think. Mind, it is not going to be fast as it is going to be hard to generate efficient code ...
- flohofwoe 5y ago
- 999900000999 5y agoMaybe I'm stupid. But why is Rust so much harder than any other newish programs language. Dart is like all of my dreams come true at once, Rust still gives me nightmares. I seriously tried to learn it multiple times and failed repeatedly. I've created several Dart/ Flutter projects for myself and friends. Multiple C#/Unity projects. Python and JavaScript have paid my rent for the better part of a decade. But Rust, I can't grasp basic concepts.
- lordnacho 5y agoI've found the opposite. Everything around the language is very ergonomic, people have written tools and libs for everything, nothing is hard to integrate. The language itself seems ok too. If I didn't have the IDE tools to tell me what was wrong I'd be screwed, but once you run into your first few borrow checker issues you'll read up on what's actually going on and you can fix the problem. Even the async stuff that the guy is writing about, I don't recognize. Normally async causes all sorts of issues, especially ones where you're totally stumped and the errors make no sense. With Rust it's just been a breeze. Write some code and at some point the compile will heavily hint not to go that way. Especially with shared state type systems, a borrowing this way and that, you'll find that it's smarter to rethink the arch rather than add yet another Arc<RwLock<>>.
- vbg 5y agoIt worth the reward to keep trying. You must be intentional about both coding and systemically learning at the same time how it works. Watch YouTube videos plus build a real world project. My first attempt to learn rust failed. My second attempt was much better.
- masklinn 5y ago> But why is Rust so much harder than any other newish programs language. All the languages you cited are essentially in the same space, they're runtime-heavy GC'd ("managed"), imperative, object-oriented, "applications" languages. Rust tries to achieve memory-safety without runtime support (as well as low-overhead in general), and to do that it relies on a rather advanced type system with concepts which aren't formal in many commonly used languages (e.g. borrowing and affine types). It's a different language, and if you approach it with the expectation that it's similar to other languages you know, it's probably not going to work well. Possibly unless the language in question is C++ (as Rust formalises a lot of best practices of C++), but even then the lack of object orientation and strictness of the compiler is going to make the transition difficult. It's probably easier for people with less expectations that "all languages are similar" and that their knowledge will be easily transferrable, either because they have less experience period, or because they have experience with a much wider range of languages.
- secondcoming 5y agoThere are constant efforts to make async easier in all languages. It'll never be as easy as writing synchronous code. I really don't like futures/promises, I don't know where this abstraction came from. Callbacks are where it's at. Someone, somewhere has to write callback code; they cannot be got rid of. What works for me is keeping the callback handler as short as possible, this usually means just pushing 'work' onto an async queue that is serviced by one or more threads.
- fnord123 5y agoCallbacks are insufficient. They do not handle pythons with or javas try-with-resources, making a lot of code break completely. E.g. metrics.
- __s 5y agoI've found async to be straight forward anytime I've used it. Promise#then is equivalent to callbacks async/await often requires very little changes compared to synchronous code, whereas reworking a program into callbacks is much more impactful. & the async/await compilation process tends to produce better performance in addition to this. My first async/await work was a few years ago to increase a data importer's performance by an order of magnitude compared to the blocking code (I also used it a couple years earlier for a GUI (PointerWare) where code would await opening new windows, it wasn't really being used for concurrency there, just a nice interface of "show pop up, wait on that view to complete" which made Back buttons easy) Here's an example where looping made for a callback that recursively called, using async/await I get to use a plain loop: before: https://github.com/serprex/Befunge/blob/946ea0024c4d87a1b75d808ce429442938d1770f/js/funge.js#L572 https://github.com/serprex/Befunge/blob/946ea0024c4d87a1b75d... after: https://github.com/serprex/Befunge/blob/9677ddddb7a26b7a17ddc346be85028b0d58a767/js/funge.js#L608 https://github.com/serprex/Befunge/blob/9677ddddb7a26b7a17dd... Callbacks make it difficult to send information up the call chain, as the caller fires it off without a ticket to wait on the result. libxcb is an example of a library which optimized on libx11 by having functions which return cookies which func_reply wait on, these functions could similarly be implemented as an async/await interface I don't see why people find it so complicated to separate begin-compute & wait-on-compute I've rewritten a nodejs game server into rust, https://github.com/serprex/openEtG/tree/master/src/rs/server/src https://github.com/serprex/openEtG/tree/master/src/rs/server... handleget/handlews are quite straight forward. But apparently it doesn't work
- quotemstr 5y agoHonestly, people want to write high level code in low level languages too often. Both Rust and C++ ought to be relegated to high performance cores of larger, "squishier" programs written in higher level languages. The Emacs model is perfect for most programs running on conventional systems. The key insight here is that garbage collection gives you access to a variety of patterns that are otherwise fiendishly difficult to implement safely, including non-leaking closures, RCU-like resource sharing, and fast (but safe!) bump-pointer allocation of short-lived objects. Honestly, something like typed Python or Typescript is going to be fast enough for most use-cases, especially if any compute-heavy parts thunk out to optimized C++ or Rust. And if those aren't fast enough, you can write against the JVM or the CLR in a variety of beautiful, expressive languages and still gain the benefit of a GCed, managed environment while retaining the ability to accelerate core kernels in native code. Writing entire big systems in C++, Rust, etc. just doesn't make much sense to me. Yeah, I understand the benefits of technical uniformity, of having to train people to use only one language, and of easy of debugging when there's only one level of the stack --- but still, I think people use low level systems languages for too much, and these articles about asynchronous work being hard to write in Rust are symptomatic of this fundamental mismatch.
- couchand 5y agoSpeed isn't the reason I love Rust. It's: 1) the expressive type system, notably the use of optional type instead of null and the use of lifetimes to make reasoning about references tractable, and 2) the community, with a focus on correctness, documentation, and being welcoming.
- tialaramex 5y ago> Both Rust and C++ ought to be relegated to high performance cores of larger, "squishier" programs So, people from Google have explained about this before. The idea you have assumes that if you speed profile the code, 99% of samples land in this function "A()" and so you just re-write that part in C++ and now it's faster, or if you measure allocations you find 99% of RAM was allocated by "B()" and so you just re-write that part in C++ and now it uses less RAM. Google already did all that low-hanging fruit. When they run the profile it comes back flat. You should rewrite A, B, D, E, F, J, K, L, M, N ... in other words the way to make the system faster is to just write it in C++ Part of this is also scale. If a system I run once week is a little slow, maybe in some sense that costs 40¢ but I don't account for it. At a mid-size non-IT firm maybe a similar performance cost is $1000 per year. Just about worth somebody enquiring if it can be sped up, but not worth arguing about it if the answer is "No". Maybe a mid-size IT firm where you work spends $1k per month on this problem. You could speed it up by rewriting the whole system, this would take some time - how many days work before it's cheaper to leave the problem than pay you to fix it? However, at Google's scale maybe that problem costs them $1M per week. They can justify assigning a whole team to fix that because of scale.
- deleted 5y ago[deleted]
- api 5y agoSpeaking of the four versions of tokio in your build problem: this is definitely an issue with Rust dependencies. Async just magnifies it because there are a lot of async utility crates. Personally I hate dependencies in general and try to minimize them. Instead of reaching for a utility crate I think “how could I architect this so I don’t need this hack?” There is almost always a way. In the end falling back on Arc<> is still cleaner and probably comes with less overhead than dependency spaghetti.
- couchand 5y agoThe author spills a lot of ink showing that they really didn't take any time at all to understand the problems they complain of. The title is clickbait, of course aync Rust works, and the article doesn't talk about it anyway. The only mention of async is an observation that it's CPS under that hood, followed by a wandering rant about CPS that fails to account for the design constraints. In the end, the author suggests forcing every AIO user to manually write out their polling loops, which is simply a silly idea. At least if their recommendation was a completion-based API without language support they would seem serious.
- gerbilly 5y agoCan someone explain to me the attraction of async programming? I don't really do JS, where a lot of this seems to be happening, but the code I have seen with the huge ladders of callbacks doesn't seem so great to work with to me. Also, although using promises seems better, it seems like it could quickly become spaghetti.
- kall 5y agoWorking with Promises and async/await doesn‘t really turn into spaghetti in my experience. Biggest downsides are the function coloring and the lack of cancellation primitives. They usually fit the problems I‘m solving pretty well and play well enough with functional programming styles. Having never worked with threads heavily, they always seemed like WAY more hassle to me. I assume that‘s the alternative? I think go has something else?
- jffry 5y agoIf you mean non-blocking in general, the benefit is that your system can do something else useful while it's waiting for some operation to complete (usually things accessing files or a database or a network service) If you specifically mean async/await syntax, let me illustrate with a contrived example. It can let you express a sequence of asynchronous operations in a more natural way: function promised(cache, db, metrics) { return cache.query(...).then(cachedResult => { if (cachedResult) { return cachedResult; } else { return db.query(...).then(dbResult => { return cache.store(dbResult).then(_ => dbResult); }); } }).then(finalResult => { metrics.log(...); return finalResult; }); } async function awaited(cache, db, metrics) { let result = await cache.query(...); if (!result) { result = await db.query(...); await cache.store(result); } metrics.log(...); return result; }
- gerbilly 5y agoIt's not that I don't see the benefit of the CPU doing something useful when waiting for I/O, my confusion comes from the fact that people like to express this using promises/await. Why not just arrange it like this? function non_awaited(cache, db, metrics) { let result = cache.query(...); if (!result) { result = db.query(...); cache.store(result); } metrics.log(...); return result; } Basically doesn't a good threading library just allow you to make all those 'awaits' implicit? I mean just because await isn't explicitly there, if I run the function above in a thread, my understanding is that the thread does yield to other threads while waiting for I/O. It's not as if it busy-loops while waiting or something.
- artursapek 5y agoAny time I've seen people saying some permutation of "Rust doesn't work" it has boiled down to the same problem: It's not that Rust doesn't work; it's that you're misusing Rust.
- artursapek 5y agoI run a team at work building a desktop application in Rust which handles live streaming market data and displays it in an arbitrary layout defined by the user. From our experience (and we have a very good Rust team), async Rust works fantastically well. https://cryptowat.ch/desktop https://cryptowat.ch/desktop
- darthrupert 5y agoThe author ends this with a now apparently trendy Common Lisp yearning, but ironically, Common Lisp's async story is pretty weak, too. On the other hand, at least it didn't infect the whole ecosystem, just the projects that use certain libraries.
- mplanchard 5y agoI write async rust for my day job, and while it is more complicated to write than synchronous rust, the complications generally make sense once you understand how the compiler is managing the remarkably tricky task of async with no GC. Rust remains significantly more pleasurable to write than e.g. typescript for me. Some patterns around error handling still a bit awkward, but the FuturesExt and TryFuturesExt traits help a lot there. My only real “I have no clue what’s going on” moment so far has been with an error about traits not being general enough, but someone on the rust forums helped me out: https://users.rust-lang.org/t/trait-is-not-general-enough-for-send-able-future/65396 https://users.rust-lang.org/t/trait-is-not-general-enough-fo... Setting up clippy to disallow non-send-sync futures throughout the codebase has prevented that particular thing from recurring.
- Communitivity 5y ago"I’d like to make a simple function that does some work in the background, and lets us know when it’s done by running another function with the results of said background work" This is why he's got a problem, in a nutshell. I suspect the author has a heavy Javascript background and is used to Continuation Passing Style (CPS)[1]. The problem is that in a non-interpreted language that is a horrible way to do things because of scope and references. A better approach is to use messages from your thread. In C++ you could do this with ZeroMQ inproc sockets [2], or other mechanisms. In JAVA you could use a number of methods, such as a ConcurrentLinkedQueue [3]. In Go, you have channels [4]. In Rust, you have..well, also channels. Rust By Example already has a similar example that uses channels [5]. The concept of a Message Passing Interface (MPI) works amazingly well, and there is at least one flavor implemented for all the major languages (and often more). The only languages I've seen that get callback style interfaces done well are Clojure, Lisp, Erlang, and Elixir. The only thing I've seen that's better than MPI, in some situations, is Tuple Spaces [6]. Tar and feather me for it, but I miss JINI [7] and JavaSpaces [8] when I have to do a system in JAVA. JINI is now Apache River [9], but the last release was 2016. [1] https://en.wikipedia.org/wiki/Continuation-passing_style https://en.wikipedia.org/wiki/Continuation-passing_style [2] http://api.zeromq.org/2-1:zmq-inproc http://api.zeromq.org/2-1:zmq-inproc [3] https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/ConcurrentLinkedQueue.html https://docs.oracle.com/javase/9/docs/api/java/util/concurre... [4] https://gobyexample.com/channels https://gobyexample.com/channels [5] https://doc.rust-lang.org/rust-by-example/std_misc/channels.html https://doc.rust-lang.org/rust-by-example/std_misc/channels.... [6] https://software-carpentry.org/blog/2011/03/tuple-spaces-or-good-ideas-dont-always-win.html https://software-carpentry.org/blog/2011/03/tuple-spaces-or-... [7] https://en.wikipedia.org/wiki/Jini https://en.wikipedia.org/wiki/Jini [8]https://river.apache.org/release-doc/current/specs/html/js-spec.html https://river.apache.org/release-doc/current/specs/html/js-s... [9] https://river.apache.org/ https://river.apache.org/
- justinhj 5y agoVery helpful answer, thanks!
- jude- 5y ago"Maybe we could just have kept Rust as it was circa 2016, and let the crazy non-blocking folks write hand-crafted epoll() loops like they do in C++. I honestly don’t know, and think it’s a difficult problem to solve." I think this is the most underappreciated part of this article. Since when did everything have to be async? There are other ways to represent concurrency that more accurately reflect what the computer is actually doing. For example, an alternative to async is to represent a workstream as a state machine, where state transitions happen between I/O. Then, your state machine can be a struct, and each state transition can be an impl function on that struct that takes one or more completed I/O requests as input and emits one or more I/O requests as output. This saves you from having to implement everything as a closure, which this article rants about. Your top-level epoll loop merely services I/O requests from state machine instances, and invokes your global application logic to start and stop state machines to carry out business logic tasks. I realize that many complicated workstreams could have many states due to all the I/O they might do, but the task of converting a high-level workstream into a state machine could be automated by the tooling.
- __s 5y ago> but the task of converting a high-level workstream into a state machine could be automated by the tooling That tooling is literally async/await. async/await transforms functions into state machines
- jude- 5y agoExcept doing so introduces a bunch of needless programming difficulties, including but not limited to the ones discussed in this article. For example, if you make the state machines and epoll loop explicit, you don't have the function coloring problem -- all your I/O requests from state machines to the poll loop (which may or may not cross thread boundaries) are explicit synchronization points with your global business logic, which gives you fine-grained control over how your state machines handle things like request timeouts, resource quota limits, cancellations, execution suspend/resume, and so on. As another example, you're not limited to factoring your state transitions into closures. As a third example, your business logic would have global, explicit control over I/O scheduling, which greatly simplifies end-to-end QoS and request prioritization.
- tus89 5y agoThe hivemind is most displeased.
- pbiggar 5y agoThis was why I chose F# instead of Rust for the Darklang rewrite [1] - I just couldn't get async to work well, and the hoops and complexity I had already had to get it even close to working were way too much. [1] https://blog.darklang.com/why-dark-didnt-choose-rust/ https://blog.darklang.com/why-dark-didnt-choose-rust/
- mgaunard 5y agoSo basically, Rust does async right, but the author wishes he didn't have to worry about generics and lifetime management which become increasingly complicated in asynchronous contexts. Maybe this guy needs to just stick to Lisp.
- dang 5y agoWhy asynchronous Rust doesn't work - https://news.ycombinator.com/item?id=26406989 https://news.ycombinator.com/item?id=26406989 - March 2021 (482 comments)