31 ms·
Why asynchronous Rust doesn't work
- Animats 6y agoI'm coming around to the position that pure "async" is OK, and pure threading is OK, and green threads (as in Go goroutines) are OK, but having more than one of those in a language is not OK. They do not get along well.
- socialdemocrat 6y agoIsn’t the Go solution green threads on top of hardware threads? Lots of green threads get load balanced on top of a thread pool. Isn’t that the whole unique aspect of Go? So in essence they do both.
- justicezyx 6y agoI think the idea is that they do not present as competing paradigms. Go green threads or go routine are the only visible constructs to users. Whereas the hardware threads are not usable to users. Edit: From the perspective of mutli-paradigm, C++ is truely a unique language. It not only has multiple paradigms, and largely contains them in a relatively harmony state.
- Animats 6y agoRight. The key point being that in Go, you can block or use some compute time without stalling out the async system.
- chubot 6y agoWell I'd say you have to take a lot of care to make them compose well. Just like it takes care to compose thread- and process-based concurrency, it takes care to compose threads and async correctly and comprehensibly. If you care about utilization you MUST use threads (or processes, which are a different story). So if you use async, then you have the problem of composing them, which is indeed hard. There are some C++ codebases that do this well but they're notable for being exceptions ... There does appear to be overuse of async as a premature optimization. The default should to use threads and explicitly shared state -- basically the idioms in Go, although I'm sure ownership complicates that a lot.
- int_19h 6y agoThe problem with green threads is that, unless they're a part of the platform ABI, they don't really get along with anything else - including green threads in other languages/frameworks! This makes cross-language/runtime interop unnecessarily difficult, and as I understand, it's partly why Go apps tend to be "pure Go", and even their stdlib insists on using syscalls on platforms where you're supposed to go via libc (like BSDs, and previously also macOS). But there doesn't seem to be any concerted effort to standardize green threads. Win32 has had fibers for a while, but nobody's actually using them, and runtimes generally don't support them. Even .NET tried to do it in 1.x, but then dropped support in later versions.
- Animats 6y agoThis makes cross-language/runtime interop unnecessarily difficult, and as I understand, it's partly why Go apps tend to be "pure Go". Yes. Fortunately Go has enough well-exercised standard libraries that this usually isn't a big problem.
- nynx 6y agoThat's an evocative title.
- yannikyeo 6y agoI would say it’s clickbaity.
- xiphias2 6y agoAs I read through the database example, I saw that the compiler just caught a multi-threading bug for the author, and instead of being thankful, he’s complaining that Rust is bad. I think he should use a higher level framework, or wait a few years for them to mature, and use a garbage collected language until then.
- joubert 6y ago> caught a multi-threading bug The compiler complained: “error[E0308]: mismatched types”
- xiphias2 6y agoThat’s why it’s amazing, type safe efficient multi-threading without dynamic memory allocation with a nice syntax...it’s the holy grail of server programming.
- literallyWTF 6y agoYeah I don’t get it either. We’re supposed to be upset that it did it’s job? Lol?
- andrewflnr 6y agoIt would be nice if it was explicitly a borrowing-related message. That's the kind of thing I can see happening in a future rustc actually.
- xiphias2 6y agoYeah, the good thing is that the devs are always happy to take feedback on improving the error messages (like explaining differences between Fn/FnMut/fn here)
- thu2111 6y agoWhy would I want to write a server in a language that requires such awkward approaches to basics like coroutines and dynamic dispatch when I could use kotlin on the JVM and get pauseless GC, ultra fast edit/compile/run cycles, efficient and powerful coroutines, and eventually Loom which will eliminate the whole problem of coloured functions completely and let me just forget about coroutines? Multi threading bugs are not so common in real Java/Kotlin programs to justify this epic set of costs, in my view.
- spamizbad 6y agoI'm surprised Rust went with a Scala-inspired async/await considering it wasn't well-liked outside of PLT enthusiasts. JavaScript adopted it as I don't think you could bolt any other type of sugar on top of the event loop in browsers... but Rust shouldn't suffer from such baggage.
- nynx 6y agoWhat's an alternative to async/await that's explicit about runtime costs?
- thu2111 6y agoHow explicit do you want? The loom model is VM managed thread stacks with user space context switching. The costs are all quantifiable and a suspended "virtual" thread uses memory directly proportional to the amount of stuff on the stack, same as any coro implementation. However there is no async/await or coloured functions. The VM just makes threads super cheap
- roca 6y agoRust started off with user ("green") threads. The problem is interop with other code via FFI --- it gets really painful and potentially expensive. That's not good for a systems language. So they ripped it out.
- pshc 6y agoDo you have a source on people generally disliking async/await? I've become a fan of it in JS.
- spamizbad 6y agoThere's the function coloring issue: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... Where your sync functions have X considerations and your async functions have separate Y considerations - BUT, async/await sugar makes these widely different behaviors look nearly identical - so you get "clean" sync code aesthetics on your asynchronous code and are sort of on your own when tracking the behavior of its implication.
- ironmagma 6y ago> The thing I really want to try and get across here is that *Rust is not a language where first-class functions are ergonomic.* So... don’t use first-class functions so much? It’s a systems language, not a functional language for describing algorithms in CS whitepapers. Or use `move` (the article does mention this). There are easy paths in most programming languages, and harder paths. Rust is no exception. The fact that passing around async closures with captured variables and also verifying nothing gets dropped prematurely and without resorting to runtime GC is bleeding-edge technology, so it should not be surprising that it has some caveats and isn’t always easy to do. The same could be said of trying to do reflection in Go, or garbage collection in C. These aren’t really the main use case for the tool.
- TheCoelacanth 6y agoYeah, a struct holding four different closures is not a pattern that makes much sense in Rust. That would look a lot better as a Trait with four methods.
- lmm 6y ago> So... don’t use first-class functions so much? It’s a systems language, not a functional language for describing algorithms in CS whitepapers. Then maybe it was a mistake to adopt an async paradigm from functional languages that relies heavily on the idea that first-class functions are cheap and easy? (FWIW I think Rust was right to pick Scala-style async; it's really the only nice way of working with async that I've seen, in any language. I think the mistake was not realising the importance of first-class functions and prioritising them higher)
- ironmagma 6y agoIt's always possible there's a better way. If you know of one, maybe you can write a proposal? There is always Rust v2. Rust lacks a BDFL and so it's almost like the language grows itself. Chances are, the async model that was used was picked because it was arrived upon via consensus.
- 6y ago
- croes 6y agoI don't know rust good enough but the article sounds like 'I can't write it like I want, therefore it's bad'. Couldn't he just pass a named function instead of an anonymous one?
- lmm 6y agoAsync isn't really the problem - the same issue pops up with error handling, with resource management, with anything where you want to pass functions around. The real problem is that Rust's ownership semantics and limited abstractions mean it doesn't really have first-class functions: there are three different function types and the language lacks the power to abstract over them, so you can't generally take an expression and turn it into a function. NLL was actually a major step backwards that has made this worse in the medium term: it papers over a bunch of ownership problems if the code is written inline, but as soon as you try to turn that inlined code into a closure all those ownership problems come back. IMO the only viable way out is through: Rust needs to get the ability to properly abstract over lifetimes and functions that it currently lacks (HKT is a necessary part of doing this cleanly). A good litmus test would be reimplementing all the language control flow keywords as plain old functions; done right this would obviate NLL. But yeah that's going to take a while, and in the meantime for 99% of things you should just write OCaml. Everyone thinks they need a non-GC language because "muh performance" but those justifications rarely if ever hold up.
- joubert 6y ago> anything where you want to pass functions around. You should read the article! The author goes into this in fascinating detail.
- lmm 6y agoI did read the article; it's overly focused on async (especially the headline). The body quite correctly analyses the problem with functions, but misses that this is much more general than async; just adopting a different async model isn't a solution.
- joubert 6y agoWell, the article’s focus is on asynchrony. Naturally then, it talks about closures within the context of asynchrony and to the extent that it is relevant. The author could have made the topic about closures, but that’s not what they wanted to talk about.
- roca 6y agoThis article is a bit confusing. The concrete issues described all relate to closures, but the author says the real problem is async and they liked Rust before that (when it had the closures they have concrete issues with?).
- gwbas1c 6y agoFor a few months while I was between jobs, I decided to learn Rust. Because async code is supposed to be better than threaded code, I spent all my time trying to write async Rust. Big mistake! I never accomplished what I set out to accomplish. Most of my experience is in C#, where the difference between async and threaded code is mostly syntax sugar. (Until you get into the details.) (And async code makes writing UI code much cleaner.) Trying to write async Rust as a novice is downright hostile. The tradeoff of threads makes async code such that it's only worth it if your Rust process will have highly concurrent load; as opposed to C# where refactoring threaded code to async is housekeeping.
- sudeepj 6y agoI am guessing you are a point D/D' in the diagram [1] Although the diagram is for the Rust overall, I think something similar is true for async Rust. All I would say is hang in there. It gets better (from my experience). Rust is not something you can code along as you think. There is a bit a of upfront thinking what the data-structures & data-flow look like. At present, I can know in advance without writing code whether implementation I am thinking in Rust will work or not (work => satisfy Rust compiler) [1] https://imgur.com/kNkV7jm https://imgur.com/kNkV7jm
- orthoxerox 6y ago> Rust is not something you can code along as you think. This is my biggest gripe with Rust. I want to write code that produces the right result first and make it safe later.
- majewsky 6y agoWe already have too much code that "can be made safe later", but never is.
- carols10cents 6y agoIf you're interested in continuing with Rust, I'd suggest that you think about making code that produces the right result first and make it fast later: use `clone` everywhere you run into borrowing problems, use `Rc` or `Arc` instead of references everywhere you run into ownership problems, make the single threaded version work before adding concurrency, etc.
- brundolf 6y agoI could nitpick a few things here (you can use Arc to get around most of the reference-related issues, for example), but I mostly agree with the overall point: some things are just not as ergonomic in Rust, and may never be. I haven't done async stuff in it yet, but I've encountered my own situations where it feels like the language "doesn't really want me to be doing X". And honestly, I think that's okay. I'm someone who wishes I could use Rust for everything, but I'm coming around to the idea that it isn't really suited to everything. It was designed for the problems faced by low-level programmers, and it continues to be something of a revelation in that area, and it has also really punched above its weight in totally unrelated areas, and that's great. But I don't know if we should expect it to be great at those other areas that are so far outside its core wheelhouse. I think it's good that Rust has an option for async, but comparing its async ergonomics to Go may just be an apples-and-oranges thing.
- eminence32 6y ago> Was spinning up a bunch of OS threads not an acceptable solution for the majority of situations? It's worth remembering that this is still a very acceptable thing to do in some cases. And spawning OS threads is sometimes easier to write and easier to read, and easier to reason about than async code. Just because async is in the language, don't feel like you need to use it everywhere.
- noncoml 6y ago> Just because async is in the language, don't feel like you need to use it everywhere. Except that you will have to because no alternative library exist
- cjg 6y agoIt's easy to convert an async function into a blocking one. For example, you could use https://docs.rs/async-std/1.9.0/async_std/task/fn.block_on.html https://docs.rs/async-std/1.9.0/async_std/task/fn.block_on.h... So just use the async functions from the library synchronously if you want.
- heavenlyblue 6y agoBut you can't really do vice-versa.
- cjg 6y agoWell, you can use https://docs.rs/async-std/1.9.0/async_std/task/fn.spawn_blocking.html https://docs.rs/async-std/1.9.0/async_std/task/fn.spawn_bloc... But this is partly the reason why libraries might be more inclined to support async rather than blocking calls.
- zokier 6y agoYou know you can just write your own code and not just glue libraries together? In general I find Rust less dependant on libraries because pretty much all the basic building blocks are exposed and available. You can pretty much just start doing syscalls directly if needed
- hpen 6y agoI find rust to be too hard to use for it to ever become huge
- HKH2 6y agoJust turn all &str to String and use .clone() liberally while you get used to the language, and you'll avoid having to think about the borrow checker for most basic applications. I think Haskell is stuck like you say because if your code is overly wordy, it will run slowly too, yet Rust is still relatively fast even if you do .clone() a lot.
- hpen 6y agoAh maybe that's how I should approach. I was definitely interested in Rust as as personal replacement for C or C++
- boulos 6y agoThe author admits that they needed Arc/clone in a footnote. So I think the more interesting title would be to rehash/interpret this as “Why asynchronous Rust is too hard”. Having played with this a bit recently, I think folks are going to end up: - assuming Tokio (now that it’s 1.xx) - mark nearly everything as async - push callers to use rt.block_on to wait for an async function from non-async code I definitely miss the easy concurrency of golang the author refers to (more so in the colored functions post), particularly since AFAICT, tokio is basically going to become the default assumption. So Rust is technically pluggable here, but won’t be in practice. Does that mean that embedded folks end up having their own !tokio that they prefer? Maybe! But they’ll get locked out of the “assume tokio” ecosystem.
- joobus 6y ago> AFAICT, tokio is basically going to become the default assumption I've noticed this as well. And then there are the libs that don't support async, like diesel. I lost time trying to find the right combination of libraries for a web project. There are a lot of things I don't like about Go, but this part is easier.
- skohan 6y agoI noticed this a few months ago as well. I was looking for a way to do simple synchronous HTTPS requests in a CLI, and it seemed like using `reqwest` with tokio was more or less the default option in Rust. I ended up using ureq, and it looked like there may have even been ways to avoid it using hyper and an alternative tls-connector but the simplest solution seemed to be "just use tokio". When something foundational, like networking, gains a defacto dependency on tokio, it seems inevitable that a large footprint of the ecosystem will eventually depend on it.
- boulos 6y agoYeah, I started with reqwest because it was easy, then wanted more control (reqwest has a hardcoded 8? KiB buffer!) and went to hyper directly, and then realized I should replace all reqwest usage with just async usage of hyper. Doing so then colored/infected everything with async. It wasn’t actually bad in the end, but if you’re just trudging along trying to do your job, you suddenly walk into a “and now update all of this”. Staring at the innards of reqwest::blocking though made me realize “if I care about performance at all, I really shouldn’t use this”. One of my main reasons was so that I’d know there was only my inclusion of tokio/the version I wanted (reqwest lagged a bit, so then I had hyper w/ tokio1.0 and reqwest with an older hyper with an older tokio... and I was worried about understanding that interaction).
- hctaw 6y agoI don't think any of this is actually a reason why async rust doesn't work. The function color problem is a bit overblown, it hasn't broken nodejs yet either. Most code is sync, sync can call async and vice versa, and the ecosystem hasn't come tumbling down yet (since most Rust isn't whichever async application runtime for web apps is currently in vogue - its a systems language after all). Either way there are bigger function "color" problems in Rust. If you want a target for criticism in async Rust, it should be the lack of easy conversion from generators to async functions (and back) and the cognitive difficulty of implementing a custom executor. I also think the tangent on closures is nonsense, closures are syntactically and semantically complex because closures are hard to keep safe without a garbage collector. Reference radioactivity is a feature, not a bug.
- TheCoelacanth 6y agoWhy is it a problem that custom executors are difficult to implement? Most other languages that I'm aware of they wouldn't even be possible to implement.
- hctaw 6y agoIt's not really a problem worth caring about. Just if you wanted to gripe about the complexity of async Rust it's the most obvious to me. "But pretty much no one even lets you do that" is a great counter argument. But, it is kind of an anti feature. It's incredible that Rust allows custom async executors and the surface area for them is tiny! That said, it's kind of black magic, even for Rust. I'm willing to bet there's fewer than 20 people in the world that could make a production executor today (not that it's going to stay that way - don't get me wrong). If there's one reason to write an async application today in Rust it's because you can write a custom executor and give it a ridiculous amount of information about how the application is running by communicating with it, while wrapping both callback and polling backed APIs from system libraries in C. But that's really hard to do if writing an executor is harder than just writing callbacks or polling code manually.
- ArchOversight 6y ago
- jstrong 6y agoI've been primarily coding in rust since 2018. I never cared for async/await, and I've never used it. (at some point, coding event loops became very natural/comfortable for me, and I have no trouble writing "manual" epoll code with mio/mio_httpc). one nice thing about rust's async/await is, you don't have to use it, and if you don't, you don't pay for it in any way. sure, I run into crates that expect me to bring in a giant runtime to make a http request or two (e.g. rusoto), but for the most part I have not had any issues productively using sans-async rust during the last few years. so, if you don't like async/await, just don't use it! it doesn't change anything about the rest of rust.
- theopsguy 6y ago+1000 in this. You can just use rust like c/c++ if you don’t want to buy into async.
- boulos 6y agoMaybe it’s just the hyper and related crates universe, but my perception (see my sibling comment) is that more and more of the ecosystem will be “async only”. Like you, I think that’s sort of fine, because you can always roll your own. But it’s also kind of sad: one of the best things about the Rust ecosystem is that you can so effortlessly use a small crate in a way you really can’t in C++ (historically, maybe modules will finally improve this).
- lalaithion 6y agoI mean, you can always just use an async crate and use async_std::task::block_on.
- jen20 6y agoThis is, frankly, rubbish. Asynchronous Rust works very well - and the implementation of async/await is more ergonomic than any of the progenitor languages.
- newpavlov 6y agoA bigger problem in my opinion is that Rust has chosen to follow the poll-based model (you can say that it was effectively designed around epoll), while the completion-based one (e.g. io-uring and IOCP) with high probability will be the way of doing async in future (especially in the light of Spectre and Meltdown). Instead of carefully weighing advantages and disadvantages of both models, the decision was effectively made on the ground of "we want to ship async support as soon as possible" [1]. Unfortunately, because of this rush, Rust got stuck with a poll-based model with a whole bunch of problems without a clear solution in sight (async drop anyone?). And instead of a proper solution for self-referencing structs (yes, a really hard problem), we did end up with the hack-ish Pin solution, which has already caused a number of problems since stabilization and now may block enabling of noalias by default [2]. Many believe that Rust async story was unnecessarily rushed. While it may have helped to increase Rust adoption in the mid term, I believe it will cause serious issues in the longer term. [1]: https://github.com/rust-lang/rust/issues/62149#issuecomment-506351174 https://github.com/rust-lang/rust/issues/62149#issuecomment-... [2]: https://github.com/rust-lang/rust/issues/63818 https://github.com/rust-lang/rust/issues/63818
- devwastaken 6y agoCorrect me if I'm wrong, but isnt any sort of async support non integral to rust? For example in something like Javscript you can't impliment your own async. But in C, C++ or Rust you can do pretty much anything you want. So if in the future io-uring and friends become the standard can't that just be a library you could then use? Similar to how in C you don't need the standard library to do threads or async.
- newpavlov 6y agoYes, you can encode state machines manually, but it will be FAR less ergonomic than the async syntax. Rust has started with a library-based approach, but it was... not great. Async code was littered with and_then methods and it was really close to the infamous JS callback hell. The ergonomic improvements which async/await brings is essentially a raison d'être for incorporating this functionality into the language.
- noncoml 6y ago> And, as I said at the start, that makes me kinda sad, because I do actually like Rust. I think that’s the most important part of the article. People like Rust but it’s becoming more complex than C++. But unlike C++ it’s more difficult to pick and choose what you use. Rust’s death will be one by thousand cuts. “I really like the language but can’t justify all that complexity in my new small and simpke project” is what will cause the popularity to drop.
- roca 6y agoIt is in no danger of become as complex than C++. Nowhere near.
- 0xdeadfeed 6y agoThe pace it is going at, it’s not that far from it
- infogulch 6y agoI am curious where you get that impression. From what I could see, recent releases have just been "smoothing over pits" / filling in obvious type holes left over from older changes. Changes in rust edition 2021 are tiny and it seems to only exist in order to establish a regular cadence.
- sai_c 6y agoDisclaimer: I've only dabbled in Rust from time to time, but have not written anything serious with it. Those kind of comments are not based on looking at Rust's features on a theoretical level. It's more from the viewpoint of "How hard is it to get something (correctly) done". Those impressions are mostly formed either from using the language and failing (or having severe difficulties) to solve a given problem, or being exposed to blog posts describing those kinds of problems. Rust sure does not have as many features as C++, but it seems that in practice, the combination of available features (and/or the problems those create) is perceived as just as complex as C++. Though not being a fair assessment, the Rust community will have to try fighting this perception, if Rust is not to be labeled way more complex than C++ in the coming years.
- thombles 6y agoI'm not totally sure what the author is asking for, apart from refcounting and heap allocations that happen behind your back. In my experience async Rust is heavily characterised by tasks (Futures) which own their data. They have to - when you spawn it, you're offloading ownership of its state to an executor that will keep it alive for some period of time that is out of the spawning code's control. That means all data/state brought in at spawn time must be enclosed by move closures and/or shared references backed by an owned type (Arc) rather than & or &mut. If you want to, nothing is stopping you emulating a higher-level language - wrap all your data in Arc<_> or Arc<Mutex<_>> and store all your functions as trait objects like Box<dyn Fn(...)>. You pay for extra heap allocations and indirection, but avoid specifying generics that spread up your type hierarchy, and no longer need to play by the borrow checker's rules. What Rust gives us is the option to _not_ pay all the costs I mentioned in the last paragraph, which is pretty cool if you're prepared to code for it.
- valand 6y ago^this IMO, Rust already provides a decent amount of way to simplify and skip things. Reference counting, async, proc_macro, etc. In my experience, programming stuffs at a higher-level-language where things are heavily abstracted, like (cough) NodeJS, is easy and simple up to a certain point where I have to do a certain low-level things fast (e.g. file/byte patching) or do a system call which is not provided by the runtime API. Often times I have to resort to making a native module or helper exe just to mitigate this. That feels like reinventing the wheel because the actual wheel I need is deep under an impenetrable layer of abstraction.
- pas 6y agoThis is where Scala (and JVM based languages) would shine in theory, the JVM has a well defined memory model, provides great low-level tools, etc. (But JVM-based software is always very bulky to deploy, both in terms of memory and size, so this shining is rarely seen in practice.)
- valand 6y ago
- cormacrelf 6y ago> And then fast forward a few years and you have an entire language ecosystem built on top of the idea of making these Future objects that actually have a load of closures inside4, and all of the problems listed above (hard to name, can contain references which make them radioactive, usually require using a generic where clause, etc) apply to them because of how “infectious” closures are. No, this is straight up not true. Almost the entirety of the futures ecosystem is built explicitly out of nameable structs that library authors implement Future on. They do not typically have a load of closures inside, usually none at all. The unnameable closures are generally in 'user-space', for stringing new futures together with less friction. There are different tradeoffs, but the ecosystem is not built on a bed of indescribable spaghetti quicksand.
- gsserge 6y agoI like to joke that the best way to encounter the ugliest parts of Rust is to implement an HTTP router. Hours and days of boxing and pinning, Futures transformations, no async fn in traits, closures not being real first class citizens, T: Send + Sync + 'static, etc. I call this The Dispatch Tax. Because any time you want more flexibility than the preferred static dispatch via generics can give you - oh, so you just want to store all these async fn(HttpRequest) -> Result<HttpResponse>, right? - you immediately start feeling the inconvenience and bulkiness of dynamic dispatch. It's like Rust is punishing you for using it. And with async/await taking this to a new level altogether, because you are immediately forced to understand how async funcs are transformed into ones that return Futures, how Futures are transformed into anon state machine structs; how closures are also transformed to anon structs. It's like there's no type system anymore, only structs. That's one of the reasons, I think, why Go has won the Control Plane. Sure, projects like K8s, Docker, the whole HashiCorp suite are old news. But it's interesting and telling that even solid Rust shops like PingCAP are using Go for their control plane. It seems to me that there's some fundamental connection between flexibility of convenient dynamic dispatch and control plane tasks. And of course having the default runtime and building blocks like net and http in the standard library is a huge win. That said, after almost three months of daily Rust it does get better. To the point when you can actually feel that some intuition and genuine understanding is there, and you can finally work on your problems instead of fighting with the language. I just wish that the initial learning curve wasn't so high.
- ironmagma 6y agoIn a systems context, where performance and memory ostensibly matter, why wouldn’t you want to be made aware of those inefficiencies? Sure, Go hides all that, but as a result it’s also possible to have memory leaks and spend extra time/memory on dynamic dispatch without being (fully) aware of it.
- blazzy 6y agoIn this example rust doesn't just make me aware of the tradeoffs. It almost feels like the language is actively standing in the way of making the trade offs I want to make. At least as the language is today. I think a bunch of upcoming features like unsized rvalues and async fns in traits will help.
- eximius 6y agoNone of this was about asynchronous rust and all of it was complaining that Closures are hard. Closures are hard, but a lot of it's for good reason. As with all of the hard things Rust does, it's because it's trying to be correct.
- bsder 6y agoThe big issue is that non-GC concurrency is still effectively a CS research topic. In a GC language, you can "just" let the GC system clean up after you're done--who owns allocation and cleanup isn't an issue. In a non-GC language, who allocates and cleans up what and who pays for that suddenly becomes a fundamental part of the domain. For example, I have seen a zillion "lock-free" data structure libraries in Rust, and I don't think I've seen even one that actually holds to even the basic amortization guarantees like java.util.concurrent.
- hashmash 6y agoI'd like to understand what you mean by this. I'm no expert on Rust lock-free designs, but what are the "amortization guarantees" that you refer to? Is there something that I can read that explains this in greater detail?
- bsder 6y agoBasically any background about java.util.concurrent (the development mailing list is archived, IIRC). Also, anything by Cliff Click and Azul. Okasaki talks about it in the context of functional data structures rather than concurrent: https://en.wikipedia.org/wiki/Purely_functional_data_structure https://en.wikipedia.org/wiki/Purely_functional_data_structu... But, let's talk about something like CopyOnWriteArrayList. If I add() a CopyOnWriteArrayList(), a copy of the underlying data comes into existence with the new element. That should certainly get charged to the thread doing the add(). Fine. Some Iterators are probably still pointing at the old underlying data. Not a big deal. So, the final Iterator() with references to old data finally finishes and gets deallocated/dropped. Now, all the underlying data that got copied is no longer relevant and needs to be deallocated/dropped. So, who does that? Does that happen on a thread that next calls get()? If so, get() is now O(n) worst case. That certainly doesn't make people happy. Does that happen on a thread that next calls add()? Well, we can have a lot of iterators that were all working and now ended. So, that add() might be deallocating a LOT of copied stuff. This might be where it has to be done, but the fact that your O() performance is dependent upon the size of the data and the number of copies that got made makes you O(n^2)-ish (O(n*k) to be more precise). That's not good. Does the deallocate happen on thread where the last Iterator() got dropped? Having to deallocate the universe because you just happened to be the last one using the underlying data store doesn't seem like a good idea. Perhaps you do a little bit of deallocation work on every get()? Probably good for throughput, but it sure makes the data structure a lot more complicated. Perhaps you have an explicit deallocation call. That kind of defeats the whole point of using a language like Rust. In a GC language, this all gets charged to the GC thread and swept under the carpet. That's what I'm thinking about when I say "amortized". There's also a slightly different amortized. In C++, for example, when you insert() into an unordered_map(), the "normal" time is O(1), but every now and then you get an O(n) while the structure reallocates. This results in an "average/amortized" cost of O(1) if the reallocation is done intelligently (generally doubling in size every overflow).
- KingOfCoders 6y agoRust chose to build on Futures and async. Author wants to have node style callbacks "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." Author is unhappy with Rust. What I would like to have out of Rust async runtimes is the Go version: Coroutines and moving waiting IO automatically to a thread queue.
- KingOfCoders 6y agoFrom an aesthetic point of view I'd like something that does not show the "what colour is your function" aesthetic. From writing a large SaaS companys code in async Scala, function color was none of my problems.
- lmc 6y agothread::spawn (or equivalent in other langs) is an inappropriate way to do async in pretty much any situation. Very sceptical about this article.
- klabb3 6y agoIn order to understand why async Rust is difficult to use today, one must understand the background and goals of Rust, in particular Rust's zero-cost abstractions: The type- and trait system got Rust very, very far, delivering static dispatch and stack allocations by default, while maintaining type safety and a high level of flexibility for a manageable complexity cost. When async Rust was incubating, these goals were upheld dogmatically, and the community set out to preserve all these traits when building async, for better and worse. However, in order to get there, Rust needed to: - Power-use existing complex features and/or verbose wrappers such `Box`, `Send`, `Arc`, `impl Trait` types and an influx in generic type parameters. (Several of these cause function-coloring issues) - Introduce new complexity for self-referential structs with `Pin` (and if you dare - check out "pin projection") - Allow for custom and extensible runtimes (aka executors - responsible for scheduling), including both single- and multithreaded. The end result is a tremendous technological achievement - essentially all requirements were upheld (to the point where async can be used even in embedded environments). However, for non-experts, it's tremendously complex - which affects almost all users, even those that don't write custom runtimes or advanced combinators. I'm not the one to judge whether the story of async Rust is a failure, but if it is, there is not a single reason. In broad terms, the project was extremely ambitious and Rust, arguably, wasn't entirely ready for it. A couple of missing features that could possibly have helped: - Generic Associated Types (currently causes verbose workarounds for combinators, streams etc) - "first-class" / ergonomic support for state machines (async fns compiles to a state-machine - relevant also for async traits) - Reliable destructors (`drop` is famously not guaranteed to run in safe Rust, preventing things like borrowing across tasks, which exacerbates the `Arc`-hell) Additionally, async I/O kernel interfaces were maturing in parallel - e.g. io_uring was released after async Rust was stabilized (afaik), which might have resulted in a different, completion-based design.
- topspin 6y ago> projects that depend on like 3 different versions of tokio and futures I ended up here recently. This part is truly awful. I can forgive a lot, and much of what appears in this blog post falls under than umbrella, but the tokio/std futures schism is offensively discouraging. I strongly suspect it is a symptom of some great dysfunction.
- jedisct1 6y agoI abandoned some projects due to that as well. That, and constant breaking changes in major libraries such as `hyper`. Maintaining Rust code is expensive. More than completing the first iteration of a project. Language such as Go or C are boring, but don’t have that issue.
- carols10cents 6y agoYou can choose to not upgrade your crates to avoid this cost.
- atweiden 6y agoI never see popol [1] mentioned in these discussions: Popol is designed as a minimal ergonomic wrapper around poll, built for use cases such as peer-to-peer networking, where you typically have no more than a few hundred concurrent connections. It’s meant to be familiar enough for those with experience using mio, but a little easier to use, and a lot smaller. It shows a lot of promise. [1]: https://cloudhead.io/popol/ https://cloudhead.io/popol/
- joshsyn 6y agoGlad to see my VB job wont be going any time soon.
- deleted 6y ago[deleted]
- RantyDave 6y agoAgain, feeling old, I'm not seeing a lot wrong with threads. A thread pool, if you insist, but explicit workflows as opposed to chaining together callbacks and contexts ... just seems really easy.
- adwn 6y agoI think you're spot on. For 99% of the use cases, traditional threads are the way to go. It's only when you have a truly massive number of logical threads (>100k, or even >1M) not doing anything most of the time, that OS threads reach their limits due to their aggregate stack size. In other words: web services, where each server thread either waits for input from the client or for a database reply, and only if they have to handle hundreds of thousands of concurrent clients. This is not a concern for most Rust users, but it is a concern for large companies that are potential patrons of an up-and-coming programming language. I'm not saying it's wrong to cater to their needs, rather that it's not something over which regular Rust users should lose sleep.
- akvadrako 6y agoYou're right but even most big players can probably just use threads. If you are serving 1 million clients per VM you are probably taxed for other resources besides threads unless 99.99% of your clients are guaranteed to idle.
- pornel 6y agoThings that are easier with Rust's async: • Timeouts and cancellation. Rust's async code can be aborted from the outside (with correct cancellation and cleanup of everything in progress). OTOH if you run imperative blocking code, you need every blocking function to cooperate. • Full utilization of CPU and I/O in workloads that mix both. Async executor balances threadpools/polling queues for these automatically. OTOH if you just use regular blocking calls with no state machines/continuations, it will be hard to separate these two. A single mixed-use threadpool will oscillate between I/O-bound threads and underutilizing CPU, and CPU-bound threads and idling I/O.
- kimundi 6y ago
- deathanatos 6y ago> Was spinning up a bunch of OS threads not an acceptable solution for the majority of situations? Could we have explored solutions more like Go, where a language-provided runtime makes blocking more of an acceptable thing to do? I think this is the real fundamental disagreement here (well, at least with the async stuff). (And the comments slightly earlier, regarding "the color of your function".) The way I think about the various async stories out there (like, all of them, Go's, Rust's, Python's, C w/ OS threads), is that they boil down to roughly three or so primitives: (I haven't formalized this … and someday I should probably write a blog post on it, so it's not going to be perfect.) 1. separate executions of code (threads, green or not, goroutines, etc.) 2. selection (the ability to block on **multiple** threads, simultaneously, but return after *one* is finished or ready. It's not join().) 3. cancellation (the ability to interrupt and cancel a thread) "Was […] OS threads not an acceptable solution?" No: the selection story is terrible (without involving epoll. You'll pretty much have to build this out around conditions, and it's painful), and the cancellation story is close to non-existent (you have to somehow transmit to the thread that it should cancel, and then that thread needs to obey it). In combination, some things are outright impossible: if you're blocking on a socket recv in a thread, how do you get notified if you were cancelled? (You literally can't, with the primitives that most OSes offer, without resorting to hacks, or epoll.) (Some OSes have calls to kill threads. They are basically one-way tickets to UB.) Golang's solutions get closer, but I don't think they're appropriate for a systems language that cares about low-level performance & memory layout to the degree that Rust does.¹ But even golang struggles here: the cancellation story is missing. Golang's approach here is retroactive: you have a context object, and you must thread it through function calls to anywhere it might be required. If your API didn't think to do that, you're SOL as a consumer, but worse, even as a coder, adding it is going to be a right PITA. This isn't to say Rust's story is perfect here, either: it is tempting to think that dropping a future is cancellation, and it is very, very close (it provides a decent default). But sometimes cancellation also needs to propagate across an I/O boundary (usually, across a network socket to a server, to cancel a request) and those are async. (I think there is some work being done in Rust with asynchronous drops.) These problems show up in JavaScript — whose Promise lacks cancellation — and Python (which has cancellation via raising CancelledError; also, Python really, IMO, messed up the terminology. Between futures, coroutines, tasks, and awaitables, there's ~2 real types (futures and tasks), and the rest are … IDK, weird distinctions that are hard to keep straight.) ¹Now, this is the heart of the other part of the article, the half about closures and fn types. Rust cares about ownership (as it allows programs to avoid not just memory issues, but a whole host of bugs resulting from ownership confusion that happen even in memory-safe languages like Python) and it cares about the performance implications of things like dynamic dispatch, or separate allocations for the data associated with the callback that you want to pass. And between trait types & generics, it lets you choose, but that does result in some verbosity. You can also choose Arc, but some of us don't want that in all cases. (I don't agree with the "color of your function" argument/article, either: async functions in Rust are fundamentally still functions, they just return a Future<T> instead of a T. That distinction is important; we're not just after the T, and the future is something we're going to block/wait on; it is its own type, with its own operations — specifically, and more or less, the ones above! —, that are crucial to how the code functions.)
- adamch 6y agoThis is pretty overblown. I write async rust every day for my job, just fine, with no real problems. Probably because I'm consuming other libraries, I'm not trying to write my own. I use well-tested libraries like Actix-Web or occasionally Tokio. I've migrated multiple projects from futures to async/await once the syntax came out. ' The problems the author is describing might apply more to library authors, but for the many Rust programmers like me who are trying to solve problems, not write a framework, async is easy and practical.
- scoutt 6y ago> The problems the author is describing might apply more to library authors I don't know much about Rust, but I found the examples in the article kind of simple. As far as I can see, the author is trying to spawn a new thread and share an object between the main thread and the new one. It's seems like an everyday paradigm. Perhaps the problems arise when passing complex objects instead of native types (i.e. u32).
- littlestymaar 6y ago> As far as I can see, the author is trying to spawn a new thread and share an object between the main thread and the new one. It's seems like an everyday paradigm. And it works very well in Rust as long as you understand the core concept of the language (ownership). Rust is special, it has this fundamental concept that you need to understand before going on, and that's add some inevitable learning curve. But once you know how it works, it just works exactly as you'd expect and in the kinds of things the author is trying to do[1], it doesn't gets in your way at all. The post just sounds like the author didn't took the time to understand the core concepts, and is trying to brute-force their way through. This way of learning doesn't work very well for Rust, or at least not if you are impatient. (Especially because the error messages when it comes to closures are still far from Rust's overall standards). [1] there are things where Rust's ownership is a constraint (cyclic datastrutures for instance, or shared memory between your program and a C program (io_uring, or wayland)). But those are situational.
- 6y ago
- dcow 6y agoWhat’s wrong with async_std? The author calls it spicy. I’ve been using Surf and Tide which are built atop it and things have been a breeze. It is essentially a standard lib where all functions are red (a solution as I’d interpret things based on the linked rant). In my experience rust async’s only wart is the incredibly confusing function signatures you get with Box<Pin<... which I would think can be cleaned up.
- topspin 6y ago> What’s wrong with async_std? The problem is that it exists. Or rather, that multiple async libraries exist and every attempt to employ asynchronous programming in Rust inevitably leaves you with the problem of aligning these different, redundant, incompatible, incomplete, evolving async libraries, which is an unmitigated disaster. Rust, all by itself, is a non-trivial hill climb for developers. They're willing to bust out the climbing tools and make the attempt because they see the value. But then they get hit in the head by debris like futures::io::AsyncRead <-> tokio::io::AsyncRead and that is it; they come down off the hill and go find something else to scale. Worse yet they don't talk about it; they love Rust, the idea of Rust, and they want it to thrive so they avoid being critical. IO and async is fundamental. It's too fundamental to suffer multiple, partial, incompatible, ever evolving realizations. This is the number one issue that threatens Rust. People have only so much patience to spare; only so many attempts they're will to make climbing the same hill. I have hope. Companies like Amazon and Microsoft are making serious commitments to Rust because they too see the value. They're going to get knocked in the head by the same falling debris. And so there is a chance that while this is all still young and the MBAs and lawyers and rent seekers aren't yet savvy, Rust will receive the sort of tedious, expensive labor it so desperately needs to mature.
- dcow 6y agoThis is why I like async_std. It’s literally the same types as the normal standard lib but it returns futures for anything that could block. If you know the normal standard lib then you know async_std. I’ve stayed the hell away from Tokio for the reasons you’re expressing, but async_std seems like a solution to the problem, not a cause.
- varajelle 6y agoCan we please stop using this "color" argument to Rust? The original colouring article was about JavaScript, a dynamically typed language. But rust is a statically typed language, and in a sense, the type is the colour. You can't return an error from a function that does not return a Result<>. And most people agree it is a great thing over dynamic/implicit exceptions. Declaring a function just changes the return type to return a future instead. Also, it's been many year Pin was stabilized, and that you can write `impl Fn(Xx)` instead of using a where clause, and return closures using that syntax.
- gpderetta 6y agoThe color problem matters for static languages as much as for dynamic languages. The essence of the problem is whether you can turn internal iterators into external ones without rewriting them. If you can't then your language has function colors.
- amelius 6y agoThe only thing a type-system brings to the table in this matter is an error at compile time if you used the wrong color. It won't solve the color problem.
- pornel 6y agoAlso Rust async runtimes have `block_on(async)` and `spawn_blocking(sync).await`, so the main problem of call graphs split by "color" doesn't exist in Rust. Rust can merge the two worlds. Unlike JS, Rust can wait for async code in blocking contexts, and it can run CPU-bound code in async contexts.
- bullen 6y agoI'll repeat this until my karma is zero: Erlang, Rust and Go have simple memory models and cannot do Joint (on the same memory) Parallelism efficiently. They are only fragmenting development. In my opinion there are only two programming languages worth mastering: C+ (C syntax compiled with cl/g++) on client and Java SE (8u181) on server. That said C++ (namespaces/string/stream) and JavaScript (HTML5) can be useful for client/web. All other languages are completely garbage! Not only do they have huge opportunity cost, but they make it harder for this civilization to be useful long term! We have linux/windows and C/Java, instead of making another language in C, make something useful that is open-source instead! Edit: Most downvoters here will be technology naive; they think everything gets better for eternity, but in reality everything hits it's peak at some point and electron processors and their OS/languages have now explored all viable avenues. Accept that and stop wasting time!
- gher-shyu3i 6y agoWhy Java 8 as opposed to the latest JDK? And what about C#? It's in a similar boat as Java. Also, how is the Java memory model different from Go for this use case? They both allow mutation by sharing.
- bullen 6y agoJava 8 is the last free Oracle JDK, nothing added after 8 is really interesting enough to take the complexity hit that Java 9/10/11 etc. mean. I'm waiting for user space network, that is the last feature that will make the switch worth: my system uses almost as much kernel copy CPU as my user space process!!! C# is an ok alternative but they went for value types instead of sticking to the VM. Java has atleast 10 years head start on C#, also Microsoft. Go has no VM but uses GC, thay miss half of the requirements to make Joint Parallelism! WebAssembly has a VM but no GC, that is more interesting, unfortunately there is no glue code in the reference implementations!
- gher-shyu3i 6y ago> Java 8 is the last free Oracle JDK, nothing added after 8 is really interesting enough to take the complexity hit that Java 9/10/11 etc. mean. The low latency GCs and the general performance improvements seem quite good though. > C# is an ok alternative but they went for value types Java is also getting value types (project Valhalla). > Go has no VM but uses GC, thay miss half of the requirements to make Joint Parallelism! What do you mean by Joint Parallelism? I didn't find relevant resources. And is it bound to having a VM?
- cryptica 6y agoFunny how the Rust community used to regard JavaScript with contempt. Yet now when you consider how well JS was able to add support for asynchronous programming, it's pretty clear that JavaScript was actually a great programming language all along. JavaScript was designed to evolve. That's the difference between intelligence and wisdom. Unlike intelligence, wisdom takes into account the unknown.
- lukebitts 6y agoThe Rust community regards the JS community with contempt? Where does that opinion come from?
- cryptica 6y agoRust was often touted as a superior alternative to Node.js writing servers. Perhaps this element of contempt is no longer there but it certainly was there before due to the competition. Similar competition existed between Node.js and Golang and there was similar contempt from Go community.
- pornel 6y agoIn some performance-critical tasks Rust is doing much better than Node.js, but I don't see this as a contempt of the JS language or its community. Rust has made certain trade-offs for zero-cost abstractions and no-GC memory management, and this pays off in certain situations. Async in Rust uses a radically different model than JS or golang, but that's not to say the other languages are bad. It's because Rust has different priorities, and needed an async model that works well without a garbage collector.
- zesterer 6y agoThe article title is clickbaity as heck, but the content is (reasonably) sound. A better title would be 'Rust's async model has some flaws that are a product of its desire to ensure safety and make the true cost of certain operations visible'. Of course, such a title would never hit the front page of hacker news. The problems the author identifies are real, but they're not unique to Rust. You can get around them fairly easily with dynamic dispatch, which is what basically every competitor to Rust's async model does by default. I agree with the author that Rust's use of async in the ecosystem is increasingly universal and it's starting to become a little problematic. As the author of Flume and a (soon to be released) concurrent hashmap implementation, I'm trying to work on adding primitives that successfully marry sync and async with one-another. There's no doubt that it's hard though, and function colouring is a problem.
- scoutt 6y agoI started recently learning Rust, and a thing I don't understand is why they decided that threads have to be done this way and not simply wrapping what is available in the OS (I know that at some point, at the core, a Rust thread is the same thing I create with pthread_create). Like, in both Windows and Linux, threads exist as a callback function (pointer to function) with its own stack, in which a void* parameter is passed. Can this be done in Rust? Can multithreading be done without closures?
- rcxdude 6y agoWhat you've described with pthread_create is basically a closure, just implemented within the limitations of C and like many things in C, wildly unsafe. You can of course interact with the underlying implementation if you want (Rust is quite capable of calling into libc directly), but then you're responsible for ensuring safety. Rust provides a safe wrapper over this with a closure which is generally easier to use and harder to misuse than the equivilent C.
- scoutt 6y agoThanks. > a closure which is generally easier to use I didn't experimented much with closures so I can't discuss about simplicity yet (but the article explains that "Closures can be pretty radioactive"). > within the limitations of C Threads are exposed like that by the operating system. The C implementation is just a way to have access to what the operating system exposes, so I think it should be more like a limitation of the operating system?
- rcxdude 6y agoIt's more the limitations of C: C doesn't have the concept of a closure, so if you are passing around a function with its state, you must pass them around separately (and generally the state must be behind a void pointer, because you can't have a generic interface). In rust and C++ you can make a function with associated state easily and pass it around, both as a raw value with a unique type (though it's difficult to name), and as a reference or heap-allocated value which can be handled generically. In all these cases you need to make sure that any pointers remain valid until the function has finished executing. In C and C++ the compiler cannot help you, in rust the compiler can catch cases where function could be called after those pointers are no longer valid. This is what causes the 'radioactive' property of closures containing references to other data (they are also radioactive in C++, just at runtime and unpredictably, as opposed to at compile time in rust). If the example code in the article was written in C++ (and the complaint about the closure type which is also present in C++ was addressed), there would be a subtle concurrency bug, because the database handle could be closed and freed before the closure function actually executes. In all these cases the solution is either making sure the closure runs before the data it needs becomes invalid (which is not generally easy to do if you're planning to use it to spawn a thread), or you move the data onto the heap, either arranging so the closure frees the data when it executes (and then guaranteeing that it executes), or using some shared reference count system to ensure it only gets freed when all code which might want to use it is done. In Rust and C++ there are mechanisms in the standard library and their closure implementations to achieve this, in C you must do it manually.
- ha_jo_331 6y agoOf course a fiber/green threads based solution would be easier to use, but Rust is supposed to provide zero cost abstractions for concurrency: Maximum performance and efficiency (and safety) coming first, usability second. THat is perfectly fine. Whereas others (Go for example) sacrifice some efficiency and performance to ergonomics.
- jedisct1 6y agoIs it still zero cost if the price to pay is a steep increase of code complexity? The current model also still requires a lot of boxing and cloning, so it is far from being “zero cost” as advertised.
- steveklabnik 6y ago> Is it still zero cost if the price to pay is a steep increase of code complexity? Yes, because the "cost" in "zero-cost" only refers to runtime cost.
- jedisct1 6y agoIf Rust had to be rewritten from scratch today, it would probably be done differently, learning from experience since it was invented. Cargo, macros, const generics and `#[cfg]` wouldn’t exist any more, replaced with comptime evaluation. The standard library would be half its current size (considering its redundant functions, deprecated functions, or constructions that Clippy wants to be replaced with other constructions). IDE integration would be considered immediately. Per-function allocators wouldn’t be an afterthought (the current proposal is really bad, but it’s too late to do something better). And async would be part of the language and the standard library, “colorless” and completion-based. The problem is that it’s too late. The Rust language itself is mature, and cannot afford major breaking changes any more. So, improvements can only be made on top of what’s already there, limiting their integration, and making the whole thing more and more bloated and complicated.
- navaati 6y ago> [async] “colorless” I’m super interested that you think that’s something that can exist, how would it work, what would it look like in your mind ?
- robinei 6y agoZig does it (https://kristoff.it/blog/zig-colorblind-async-await/ https://kristoff.it/blog/zig-colorblind-async-await/). Basically using "await" just means you are declaring opportunity for concurrency at the call site, but it is still valid to await a synchronous function. And it is valid to call a function synchronously, even if it internally supports concurrency via await.
- kristoff_it 6y agoYes, that's a very clear and concise way of putting it, thanks.
- littlestymaar 6y agoI think the GP refers to Zig's approach[1], which is just “do mark functions as sync or async in there signature, just switch between two modes at compile-time using a global flag”, which I don't think fits well into Rust's values of being explicit about what is happening. [1]: https://kristoff.it/blog/zig-colorblind-async-await/ https://kristoff.it/blog/zig-colorblind-async-await/
- pjmlp 6y agoAs I already mentioned multiple times, the future are tracing GCs with an ownership concept for niche use cases. Rust's ideal use case are the scenarios that use stuff like MISRA-C, kernel and drivers code. The design team has done a wonderful work pushing for affine types into mainstream, however outside of forbidden-GC scenarios, the productivity loss versus any kind of automatic memory management approach, isn't worth it.
- eternalban 6y agoThe problem may be with the fuzzy conceptual notion of "systems language" that is the starting point of many of these languages that end up with effectively conflicting set of goals. (I agree with Animats comment elsewhere in this thread blessing all threaing models as "OK in isolation". It's just they don't play well together in a single language.)
- pjmlp 6y agoThe OSes designed at Xerox PARC, ETHZ, Microsoft Research, Olivetti, UK Navy, some of which used for quite some time in production, were quite clear, there wasn't any fuzziness on them.
- pornel 6y agoSingle ownership and deterministic destruction are quite useful on their own though, outside of just memory management. I wouldn't call them niche. Rust makes use of them all the time. Deterministic destruction is obviously very useful for resource management, and not just files and database handles, but also locks and scope guards. Single ownership allows working with mutable data without dangers of shared mutability. Without it you either need true immutability, or defensive coding by proactively copying inputs to ensure nobody else can unexpectedly mutate data you have a reference to. Single ownership also happens to nicely enforce interfaces that require functions to be called only once or that objects can't be used in invalid state, e.g. after a call to .deinit() or after intermediate steps in builders/fluent interfaces/iterator chains.
- 6y ago
- layoutIfNeeded 6y ago[me, a C++ programmer]: good, good, let the hate flow through you >:)
- mutatio 6y agoI can sympathize with the tone. I recently attempted to move to the latest `tokio-postgres` as the old synchronous version is no longer supported (the new synchronous version uses Tokio under the hood which plays havoc if you use with actix-web [two different executors]), the problem is I'm using it with CockroachDB which effectively requires transaction retries (for savepoints). I've yet to produce a ergonomic tx::retry wrapper that doesn't have lifetime issues (for primitives supporting Copy it's all good). With the asyncification of the Rust ecosystem it does make life difficult. Maybe the Rust async implementation is fundamentally sound, but the user experience for non-trivial cases isn't quite there.
- say_it_as_it_is 6y agoYou can't make everyone happy and you shouldn't try to. If people enjoy OCaml or Lisp or Haskell, they really ought to continue using them.
- hardwaresofton 6y agoThis was a great article, very easy to understand without leaving out the fundamental pieces (the saga that is async implementation difficulty). I think I can even boil this situation down to the simple thought that design & implementation of async in Rust has driven it to it's first "monad" moment. The question is now largely whether there exists a way that is better than monads to handle this problem. It's in this (HN) comment thread and a bunch of informed comments on the original article: https://github.com/eeeeeta/blog-comments/issues/10#issuecomment-795098853 https://github.com/eeeeeta/blog-comments/issues/10#issuecomm... https://github.com/eeeeeta/blog-comments/issues/10#issuecomment-795151809 https://github.com/eeeeeta/blog-comments/issues/10#issuecomm... https://github.com/eeeeeta/blog-comments/issues/10#issuecomment-795369243 https://github.com/eeeeeta/blog-comments/issues/10#issuecomm... The comments are correct in my view -- Rust can't/shouldn't do what Haskell did, which was to create use a general purpose abstraction that is essentially able to carry "the world" (as state) along with easy-to-trade chained functions on that state (whenever it gets realized). Haskell might have solved the problem, but it has paid a large price in language difficulty (perceived or actual) because of it, not mentioning the structural limitations of what Rust can do and it's constraints. The trade-off just isn't worth it for the kind of language Rust aims to be. Realistically, I think this issue is big but not enough to write off rust for me personally (as the author seems to have) -- I'd just do the move + Arc<T> shenanigans because if you've been building java applications with IoC and/or other patterns that generally require global-ish singletons (given example was a DB being used by a server), this isn't the worst complexity trade-off you've had to make, though the Rust compiler is a lot more ambitious, and Rust has a cleaner, better, more concise type system as far as I'm concerned. I think another thing I've gained from this article is another nice little case where Haskell (if you've taken the time to grok it sufficiently, which can be a long time) offers a bit of a nicer general solution than Rust, assuming you were in a world where those two were actually even substitutes. In the much more likely world where you might compare Rust and Go2, this might be a win for Go2, but the rest of the language features would put rust on top for me.
- bfrog 6y agoI'd say the entire opposite of this author, async rust works well, it solves 99% of the problems I want it to solve in a practical and useful way, right now. And even better the resulting program is faster than anything I could have cooked up on my own. Probably the only true comparable might be the scylladb seastar framework. And it's just that, with all the burdens that come with C++ coding. You won't find every client and server protocol under the sun implemented with it either which arguable async rust is building towards.
- devpbrilius 6y agoA main developer on site with paid job is necessarily its consumer, not exact producer.
- paulsutter 6y agoWe all start out loving callbacks, eventually progress to coroutines, and finally end up writing polling loops. It took me more than 30 years to figure this out. I understand completely why most people disagree - I always disagreed even louder.
- pjlegato 6y agoThe Clojure dialect of Lisp comes with a software transactional memory (STM). This basically provides relational database atomicity semantics around in-memory variable access. 99% of the pain of concurrent programming goes away. At a high level, you provide a pure function that takes the current value. The system re-runs it as many times as needed to sort out collisions with other threads, then stores the new value -- which then becomes the argument for the pure update functions running in all the other threads. Has anyone considered an STM for Rust?
- steveklabnik 6y agoThere have been some vague discussions, but STM is (as you mention), tough in an impure language. It is also not clear that it achieves Rust's efficiency goals.
- vmchale 6y ago>Before, the function we passed to do_work_and_then was pure: it didn’t have any associated data, so you could just pass it around as a function pointer (fn(i32)) and all was grand. However, this new function in that last example is a closure Those are different things! Anything pure (i.e. returns same output) could still require a closure if it captures heap-allocated data - which in Rust would entail free-ing and transferring ownership and all that.
- jlrubin 6y agoI've been working on a reasonably complicated project that is using rust, and I think about de-asyncing my code somewhat often because there are some nasty side effects you can see when you try to connect async & non async code. E.g., tokio makes it very difficult to correctly & safely (in a way the compiler, not runtime) catches launch an async task in a blocking function (with no runtime) that itself may be inside of a async routine. It makes using libraries kind of tough, and I think you end up with a model where you have a thread-per-library so the library knows it has a valid runtime, which is totally weird. All that said, the author's article reads as a bit daft. I think anyone who has tried building something complicated in C++ / Go will look at those examples and marvel at how awesome Rust's ability to understand lifetimes is (i.e. better than your own) and keep you from using resources in an unintended way. E.g., you want to keep some data alive for a closure and locally? Arc. Both need to writable? Arc Mutex. You are a genius and can guarantee this Fn will never leak and it's safe to have it not capture something by value that is used later in the program and you really need the performance of not using Arc? Cast it to a ptr and read in an unsafe block in the closure. Rust doesn't stop you from doing whatever you want in this regard, it just makes you explicitly ask for what you want rather than doing something stupid automatically and making you have a hard to find bug later down the line.
- pumpkinpal 6y agoWould anyone be able to make some brief comparisons between Rust's asynchronous model and the model followed by Asio in C++ (on which C++23 executors/networking is based) and if there are any parallels with the sender/receiver concept (https://www.youtube.com/watch?v=h-ExnuD6jms https://www.youtube.com/watch?v=h-ExnuD6jms)? I've seen a few comments talking about the choice of Rust adopting a poll model as opposed to a completion model. Am I correct in assuming these are the same things as a reactor (kqeue/epoll etc.) vs a proactor (IOCP) and that a proactor can be implemented in terms of a reactor.
- steveklabnik 6y agoI don't know enough about Asio to make a good comparison, but > Am I correct in assuming these are the same things as a reactor (kqeue/epoll etc.) vs a proactor (IOCP) Close enough, yes. > a proactor can be implemented in terms of a reactor. Yes.
- joshsyn 6y agoYep, moment Rust adopted non-monadic style of async, I stopped looking at rust.
- magwa101 6y agoBeing a developer of 30+ years I stick with C / C++. They give you the flexibility required for all use cases. They leverage the OS and hardware architecture by reflecting it appropriately. The desire to abstract away complexity is not possible given that the computing architecture is so fundamental to everything layered above it.