5 ms·
Exactly. And worse, async in Rust is viral, because any code outside of async doing I/O is not good to use inside of async. And it gets worse: making everything
by jchw 4y ago
Exactly. And worse, async in Rust is viral, because any code outside of async doing I/O is not good to use inside of async. And it gets worse: making everything async is not a good option. Effectively, it seems there is no viable answer: you need two of everything.
Go does do concurrency well, but unfortunately it’s only able to do that because of opinionated decisions that a language with Rust’s design goals cannot really make. They also have tradeoffs that I don’t think Rust developers would accept, like making calls into C functions slower. So I feel as though Rust async is at an impasse. It can be made much better, but it feels like the improvements will come at increasing complexity in the language, compiler and ecosystem. Meanwhile, the ideal end state seems like it will still have a lot of annoyances, such as the need to tirelessly duplicate anything that needs I/O for sync and async.
I almost kind of wish Rust would just drop async, as the rest of Rust is much better, having only minor issues that are very much fixable IMO. Instead the ecosystem is accelerating into it, and now it’s hard to avoid for some use cases.
I hope it all works out, because Rust is good and I’ve been advocating for its adoption at work and continually trying to adopt it elsewhere. But the issues with async are very dire, imo. Async rust is cool, but it isn’t what I would consider to be a similar level of robust, thoughtful design.
- daenz 4y agoIf Rust does decide to continue with async, they should really look into investing in improved docs and education around it. When I was looking at it a few years ago (maybe it has improved since then), I really did feel stupid for not getting the ideas/conventions/reasoning. If it was that off-putting to me, as someone with some experience, I can imagine it is very unapproachable to complete newcomers. It just felt like I was looking at something that shipped way too early.
- ollien 4y agoThe async chapter in Rust for Rustaceans was a great introduction. It leaves something to be desired because you walk away with it without knowing how to run any async code (it doesn't mention any runtimes, for instance), but it explains the concepts really well.
- Karrot_Kream 4y agoAsync is new enough that the function and trait docs are getting better all the time. There was a time it was really ugly to use async, and I still find it a bit crufty but it's a lot more approachable for someone new to Rust.
- zozbot234 4y agoIt's happening, see https://github.com/rust-lang/wg-async https://github.com/rust-lang/wg-async . Everyone is aware that current async support in Rust is very much at "MVP" status.
- berkut 4y agoThe viral aspect is the thing which really annoys me as well about it: I have some apps that make one or two single requests to DBs in certain configurations, and I've had to end up using async for those parts (up to main() obviously, although not all code in the apps needs to be aware of async thankfully) due to many crates needing/using async now for this (which may well be good/fine for other peoples' heavy usage, but not really for my usage). However, it means that even when these requests are not being done (and might never be for the running of the apps, as it's user-configured what the apps do and whether they make DB requests occasionally), these apps end up having some (it may not be much, but it's somewhat noticeable) overheads, i.e. coreCount threads are always created by the async infrastructure (despite being a single-threaded app in one of the apps cases), call stacks are deeper even though async is not really being used (although due to main() being async effectively that changes everything), meaning more memory usage for those threads's stacks (which is somewhat ironic, as that's one of the points of utilising async for heavy IO requests - reducing memory usage! - but in reverse it hurts my use cases a tiny bit). Edit: I tried to use things like pollster (in an attempt to significantly isolate and limit the async usage to just where it was needed), and it wouldn't work for my use cases. I'm on the verge of splitting the apps into two parts due to this, but due to the shared state, that would involve additional complexity (RPC or something), which I don't really want to stomach.
- SuperFluffy 4y agoYou can spin up a single threaded runtime to perform these async calls without the need to "infect" the rest of the program. The strategy is to spin up a single threaded async runtime and to just perform that call and block on the runtime itself. The easiest way to do that is probably https://github.com/zesterer/pollster https://github.com/zesterer/pollster And there really isn't much in there so you didn't need to worry about performance or anything like that. I recently refactored a colleague's program from async to sync because it's essentially and entirely sequential. The reason it started its life as async was the reqwest library, which first and foremost provides async methods to perform http requests. However, tucked away behind a feature flag aptly named `blocking` there is a small API that wraps the async api and allows making sync/blocking calls in a non-async main. And there they employ the same strategy of having a thin async runtime that blocks on completion of the async call.
- athrowaway3z 4y agoI agree to a large extend. However its not that viral if you're the app developer. Tokio ( and iirc most other executors ) have a block_on and a spawn_blocking to jump between 'worlds'. If all you're doing is something like "get 10 http request and concat them to stdout" then doing it with Rust async + a block_on call is pretty straight forward.
- miohtama 4y ago> because any code outside of async doing I/O is not good to use inside of async This is (somewhat) similar in all programming languages and it is called coloured function problem: https://news.ycombinator.com/item?id=8984648 https://news.ycombinator.com/item?id=8984648
- kibwen 4y agoThe way you solve that problem is either 1) just use threads, which is fine for almost every program with the notable exception of webservers (and even for webservers an aversion to threads is often a premature optimization), or 2) make everything async at the language level by transparently intercepting all I/O, which is acceptable for a managed language (so, most languages) but undesirable for a language with bare-metal aspirations like Rust (we're writing a bare-metal hypervisor-ish thing, and we're using Rust because very few languages are acceptable for such a constrained use case (we're not using async for it, but hilariously we could be, since Rust's stackless approach is suitable even for systems too constrained to have threads in the first place)).
- Klonoar 4y ago>I almost kind of wish Rust would just drop async, as the rest of Rust is much better, having only minor issues that are very much fixable IMO. Instead the ecosystem is accelerating into it, and now it’s hard to avoid for some use cases. I've written a decent-sized project backed by Tokio/async Rust and when I'm in pure async code, it's honestly fine and I have few complaints (just... gimme stable trait async fns). I don't think it's worth dropping; async Rust has its place and is continuing to be iterated on. What needs to happen is what's happening right now: people who write in Rust need to be louder about the fact that crates should be sync first, async second. And/or (my personal irrational opinion) the language needs to get over it and adopt Tokio into Rust proper. If this proves difficult - the ability to be sync/async via feature flags - then this is something that the language needs to solve before other features IMO (I will admit I'm saying this without checking to see if it's being worked on, though).
- jhugo 4y agoIt's usually easier to write the async version first and then write a sync wrapper. See for example the `postgres` crate.
- Klonoar 4y agoRight, I get that. I'm saying that the sync version shouldn't be "run tokio blocking in the background", because while you might be giving them an out on async infection in theory, you're still making them bring in the entire dependency tree and you're still creating more background resources than is necessary.
- jhugo 4y agoThe `postgres` crate launches a "current thread" tokio runtime which doesn't really have much overhead in terms of background resources. I agree about dependencies though. "async infection" is kinda interesting when you think about it. To use `postgres` as an example, talking to a database is asynchronous whether you like it or not. To use blocking APIs to access it is just to allow something (typically the OS) to pretend that it isn't. In general I've found the experience of writing a program async from the beginning to be very good in Rust, and in the very rare cases (in my field of work) where I have some genuine blocking to do, Tokio provides good tools for that (`tokio::task::spawn_blocking` and `tokio::task::block_in_place`). I can imagine that if you were writing a largely CPU-bound program, async would feel more like an "infection".
- SnowHill9902 4y agoHonest question: do you really need concurrency to solve problems?
- sliken 4y agoToday's world even power constrained devices like phones have many cores. Laptops and desktops are getting more cores just about every year. Even chromebooks are heading towards 8 cores last I heard. Take for instance wanting to backup a directory to a remote server and/or cloud. Sure you could use scp, single threaded would be fine, read a file, hand it over to scp, which does encryption and transport. However you'd waste quite a bit of network bandwidth and disk space. Ideally you want incremental, so you need a database for metadata. You could add compression as well. But you likely want deduplication so if you have a system image for a virtual machine and 1% of it changes you can upload 1%. You also likely want to detect corruption if you upload a backup, then restore and the file has changed. There are single threaded programs that do these kind of things, but they tend to be very slow, some well known backup clients that promise a flat rate for infinite backups often use the approach to reduce the amount of data it's practical to backup. So ideally you have multiple threads, one for scanning the filesystem, one for talking to a database of metadata, another for checksumming plaintext, encrypting, and checksumming the encrypted blob, and yet another for managing a queue of blobs to upload. Without concurrency even simple things like uploading backups can become painfully slow.
- jchw 4y agoSure... The most obvious case is running an internet service, but if you're asking if I've ever done something where there's either a strong CPU-bound or latency constraints, the answer is "yes."
- hinkley 4y agoThere’s a common problem of software design where people believe they need to put their logic and their IO together, and not only is that not true, but it screws up testing, scalability, and feature evolution when you do. A language that punishes you for not keeping your IO separate? Well, that isn’t necessarily a bad thing. Often when I find people complaining about how hard something is, I find out they’re trying to ice skate uphill, and if they backed up a second and took some advice they wouldn’t be struggling so much.
- pjmlp 4y agoMicrosoft tried to educate developers with best practices by making WinRT async only and it backfired, despite the tooling available in .NET Native and C++/CX for it. Similarly the way AsyncTask or GCD worked on mobile platforms before other alternatives came into place. Android even went as far as starting to kill processes that were doing network accesses on the UI thread. All because most will take the easy path if there is an opportunity to do so.
- aaaaaaaaaaab 4y agoIf you don’t mix logic with I/O then why do you even need async-await as a language feature? I’m not disagreeing with you, but the whole point of async-await was to be able to mix asynchronous operations into your code without having to turn your logic inside out. But if you isolate your I/O from your logic, then what benefit do you get from async-await? I guess it can still make the “I/O logic” code more readable…
- hinkley 4y agoIO callbacks don’t compose. That’s why everyone is switching to async await (that, and faddishness). This problem is amplified by mixing business logic and IO, so the motivation is in the wrong place and continues through the refactor because we’re solving only half the problem. Most of the interesting business logic is collating data from three or four places, and fetching that data sequentially is high latency. So you can do that part asynchronously and then present the input to sequential (pure) code that derives the answer. That pure code is very easy to unit test. The “problem” is now I have three kinds of code - IO, logic, and orchestration - and people intuit that this is more complicated than the bookkeeping they do and externalization onto others by mashing all three concerns together in one file, and then duplicate it with elaborate fakes in their tests. Which they are hesitant to write or others are hesitant to maintain, leading to low value, high effort tests.
- Georgelemental 4y agoThere's been some discussion among the Rust language developers about a mechanism to let functions be "generic over asynchronicity", to help alleviate all the duplication. But it's still in the very earliest design stage, and might never happen
- ibotty 4y agoDo you have a link to a proposal or the discussion?
- Georgelemental 4y agohttps://github.com/rust-lang/lang-team/issues/162 https://github.com/rust-lang/lang-team/issues/162 https://hackmd.io/Aw2L3VmPQsm0ANC_XMughQ https://hackmd.io/Aw2L3VmPQsm0ANC_XMughQ
- Ygg2 4y ago> And worse, async in Rust is viral So just like in Java, C#, Python, ... In other words asynchrony is viral.
- jchw 4y agoI feel like I should respond here. Yes. The async mechanism is similar to Java, C# and Python. However, all of those languages you listed are JIT’d languages with automatic memory management. In Rust, the mantra is very much “zero (runtime) cost abstraction” much like C++, and unlike Java, unlike C#, unlike JavaScript, unlike Python, unlike Ruby, etc. And being a language that deals with performance critical, possibly latency sensitive applications, running on potentially very strapped embedded devices, Rust libraries very much do try to provide you with the absolute best case performance. With JS, everything being async is completely reasonable because control flow always returns to the event loop anyways. With JS, Java and C#, the runtime performance is plenty fast to do some harder compute tasks, but most of the compute is spent in large abstractions and the applications are still largely I/O bound. With Python, all of that applies except the runtime performance is a fair bit worse, too. At the bottom of all of these languages is a runtime written in low level code and often times third-party modules using C++. Rust is different. Firstly, Rust is the language at the bottom. Given all of that, almost any library has to support sync and async for ideal characteristics; there’s no good way to use sync libraries in async and visa versa. But because of the differences Rust has to other higher level language, it’s much harder to accomplish this without thoroughly duplicating code. While it might never matter if you insist on never writing stuff you’d consider library code, it’s still a design issue, it still impacts the ecosystem, and indirectly, it still will impact you. I hold that I believe the ecosystem is already strained to try to make fully idiomatic, zero-compromises libraries, and the additional pressure of async bends the story until it breaks. It’s true that Rust’s design for async is not terribly unique, but I feel it is in a uniquely poor position, for unfortunate reasons that are not necessarily the fault of the Rust developers or designers.
- Ygg2 4y ago> However, all of those languages you listed are JIT’d languages with automatic memory management. Granted. However, if you mix and match sync with async code in Java, C#. You're going to have a very bad time™. At runtime. Article https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... Predates stable Rust. Rust didn't somehow add virality to async. It was viral from the start. And using Rust async with Arc/Box/Pin/etc. works more or less like in GC-land. > I hold that I believe the ecosystem is already strained Strained? Not really. Async imo is a bit overblown. Most libs I am contributing don't use async at all. My opinion is that async was rushed. Pin needed for Async was a well-spring of unsoundness issues, probably still is. Async fn in traits is missing. GATs, etc. Features that would remove some issues mentioned in the article are being worked on. But then people would be it's 2022 and no async in Rust, joke lang amirite. Damned if you do, damned if you don't.