>“enabling a feature should not disable functionality, and it should usually be
>safe to enable any combination of features”.
Isn't the real problem that async Rust colors all the functions red?
In Python it takes more thinking and structure because async isn’t built in deeply like it is with javascript.
Even so, async versus sync doesn’t feel like a nightmare in either.
Having said that, it depends entirely on how much experience you have with async programming.
Async programming is a mind bender that will throw you completely until it becomes natural. Synchronous programming feels natural and logical and async requires a completely different mental model.
I can understand why anyone forced to do async would hate it. It’s something you have to want to do.
Bonus points if it has the ability for users to define static analysis a la borrow checking.
It's great that the work finally led to both being supported. The cynic in me wonders if it's also async runtime agnostic, and not just Tokio. If that's possible.
Async makes things possible that are hard or impossible to do with synch programming.
It’s a real game changer for python especially. Cant comment on rust, hopefully its implementation is smooth.
I maintain a simple abstract middleware caching library for python and it was a trick issue how to support combinations of sync/async app code and sync/async actual caching backends with async introduction in Python. In the end the answer was an automatic runtime AST transformation for all cases. Ugh. The same approach I use in validation library to parse requests for different web frameworks. But it's a specific case and could not be generalized.
It's the opposite of fun situation.
It was multi-threaded but no async/await. When I tried to start converting things over, it was really painful. I have no idea where it landed, but I remember coming away from it thinking that I'd prefer to just not deal with the async code in its current state.
I mostly write Go now and haven't had to think about this. Sounds like it's still painful.
How is this not more natural than creating various state machines and passing around all sorts of weird highly abstract Future-objects?
On embedded, I make my own libraries for everything; the open-source community has gone full-in on either Async, or an typestate/generic API. I don't see this changing any time soon, but maybe later down the road. I feel like the only one who doesn't like either.
Couldn't you perhaps make an async runtime that isn't async and just blocks? That would let you keep only the async interface.
Python is so slow you gain nothing with async. I have a plenty of cool stories how I've fixed crumbled products because they were async and all connection load instead of nginx/backend goes directly into db.
You rarely need long connections and if you choose async for python because it's fast then it's a wrong choice because it's not.
BTW asyncio is awful bloated implementation with huge overhead around task management.
Async code maximizes the potential of the thread it’s running in.
Device IC code generally ends as files in my firmware directly.
Personally I've never liked the syntactic sugar on top of function calls very much. If something is a promise or whatever, return me the promise and I can choose what to do with it.
Is there already an article that describes this well?
This sounds like an issue with the implementation of threads and scheduling in common operating systems, and I don't see how replicating all that functionality inside of each sufficiently large programming language is remotely taken seriously.
But also, you didn't respond to what I even said. You claimed that async is 'beautiful and natural'. I disagreed. You...fell back to a performance claim that's uncontroversially true, but irrelevant to what I said.
https://nullderef.com/blog/rust-async-sync/#_duplicating_the...
It’s not necessarily about speed, though this statement above is flat out wrong.
async in Python allows you to build different types of applications. For example you can attach a task to stdout of another process and read and process it.
Ha! Integrating async I/O sources, namely the OPFS API, has been the single biggest development-time hit and runtime performance hit in sqlite.org's JS/WASM build of sqlite.
As soon as a single method is async, it cannot be hidden behind a synchronous interface because the async attribute is "viral," requiring the whole API above it to be async (which sqlite is not). We instead have to move all of the async I/O into its own worker and "fake" synchronous access to it using a complex proxy built from SharedArrayBuffer and the Atomics API. It's an abomination but it's the only(?) approach for making async functions behave fully synchronously which doesn't require third-party voodoo like Asyncify.
PS: the opposite - hiding sync stuff behind an async interface - is trivial. Hiding async behind a synchronous interface, however, is a tremendous pain in the bottom in JS.
How would you solve the problem described in the article in JavaScript? I know `XMLHttpRequest` technically can be used to make synchronus requests, but the behavior is deprecated and is actively being hobbled by browsers.
async traits in std instead of each runtime having their own,
a pluggable interface so that async in code doesn’t have to specify what runtime it’s being built against
potentially an effect system to make different effects composable more easily (eg error effects + async effects) without needing to duplicate code to accomplish composition
Keyword generics as the current thing being explored instead of an effect system to support composition of effects
With these fixes async rust will get less annoying but it’s slow difficult work.
However, many real-world programs are inherently complicated state machines, where each individual state waits on one or more nested state machines to progress and/or complete -- async is often the most reasonable way to express that.
I wrote a post detailing a non-trivial production example use of async that might be interesting, regarding the cargo-nextest test runner that I wrote and maintain: https://sunshowers.io/posts/nextest-and-tokio/.
Nextest is not "web-scale" (c10k), since the number of tests one is concurrently running is usually bounded by the number of processor hyperthreads. So the fact that async tasks are more lightweight than OS threads doesn't have much bearing in this case. But even beyond that, being able to express state machines via async makes life so much simpler.
Over a year after switching nextest to using async, I have no regrets. The state machine has gotten around twice as complicated since then--for example, nextest now handles SIGTSTP and SIGCONT carefully--and async has coped admirably with it.
(There are currently somewhat serious issues with Rust async, such as a really messy cancellation story. But that's generally not what gets talked about on places like HN.)
How max many processes you handled in this fashion? The catch is: if you need thousands than asyncio has too much overhead and you need manual epoll. If less threads are much easier to use and acceptable performance wise.
I'm not going to pretend I'm an expert but would be happy if someone could expand further.
That exists today, it's called Rust. You don't have to use async.
A nitpick, but please, if you do this for your library, name the sync one just the name and put "-async" in the name of the other one, so "rspotify" and "rspotify-async" instead of "rspotify-sync" and "rspotify-async". This mirrors the way the function definition keywords are "fn" and "async fn" instead of "sync fn" and "async fn". Most simple usecases don't need async and the extra typing and extra reading is annoying.
I'll give you a real world example. I wrote some code that listened to a websockets URL from thousands of Reddit posts - specifically, the one that sends new messages on new comments - so I could see a stream of Reddit comments for any given sub.
Implemented it using Tungstenite (synchronous) and it created thousands of threads to listen, and used enormous chunks of memory (several GB) for the stack space + memory reading for every single WS stream.
Implemented it using Tokio_tungstenite, the async alternative, and it used a handful of MB of memory and barely any CPU to listen to thousands of WS servers.
* An overview of terminology, and a description of how various languages fit into the various parts of the design space https://www.infoq.com/presentations/rust-2019/
* A deep dive into what Rust does https://www.infoq.com/presentations/rust-async-await/
If I were using the author's library, I would call `.some_endpoint(...)` and that would return a `SpotifyResult<String>`, so I'm struggling to understand why `some_endpoint` is async. I could see if two different threads were calling `some_endpoint` then awaiting would allow them to both use resources, but if you're running two threads, doesn't that already accomplish the same thing? I'm pretty naive to concurrency.
Code: https://gist.github.com/sigaloid/d0e2a7eb42fed8c2397fbf84239...
In the example you give, yes, it's just sequential and everything relies on a previous thing. But say you are making a spotify frontend - you want to render a list of playlists, the profile info, etc - you call await on all of them and they can complete simultaneously.
Async is useful when you want to have a bunch of things happening (approximately) "at the same time" on a single thread.
With async you can await on two different SpotifyResults at the same time without multithreading. When each one is ready, the runtime will execute the remainder of the function that was awaiting. This means the actual HTTP requests can be in flight at the same time.
(A honest question, I start to think that I'd like to learn more on this language)
So the mutable (or is it “volatile”?) environment is there, but you explicitly know when and where you interact with it.
https://github.com/rust-lang/rfcs/blob/0806be4f282144cfcd55b...
The immutability has nothing to do with async. Async is for IO threads. If you want pure parallelism you use `par`. But Haskell IO threads (forkIO and friends) are also green when run with GHC.
There has been work to enable generic compilation of all Haskell code to arbitrary categories (see Conal Elliot's compiling with categories) but unfortunately the approach has not caught on.
It would actually be an interesting design space to support arbitrary categories as a basic design principle of a novel programming language. Obviously at some level something would need to be the 'special' one that can be compiled to machine code, but if the complexity could be hidden / abstracted that would indeed be interesting.
If I'm awaiting on two different results, I have to invoke them in parallel somehow, right? What is that mechanism and why doesn't that already provide asynchrony? Like, if the method was sync, couldn't I still run it async somehow?
Congratulations, nobody is going to sneakily update an object on you, but also, nobody knows about your updates either.
It’s not a worthwhile trade off given the massive extra work it causes.
† Yes, there are other differences between goroutines and typical OS threads, such as stack sizes, but I'm only talking about I/O differences here.
https://github.com/Arnavion/k8s-openapi/blob/v0.19.0/README....
(Past tense because I removed all the API features from k8s-openapi after that release, for unrelated reasons.)
The gist is that while you await the result of an async function, you yield to the executor, which is then free to work on other tasks until whatever the await is waiting for has completed.
The group of tasks being managed by the executor is all different async functions, which all yield to the executor at various times when they are waiting for some external resource in order to make forward progress, to allow others to make progress in the meantime.
This is why people say it’s good for IO-bound workloads, which spend the majority of their time waiting for external systems (the disk, the network, etc)
It is a somewhat strange compile time jumble of libraries but a compile time overhead isn't so bad.
Anyway, I have been meaning to try out nextest for our big honking monorepo workspace at work. The cargo test runner has always been essentially fine for our needs, but speeding up test execution in CI could be a huge win for us.
Can you not add a suffix only to one variant, e.g., "_sync", as far as I understood you just need different names?
The whole idea of CQRS is to build separate (segregated) pathways for updates. Immutable passing plays extremely well with CQRS. The alternative is the complete clusterfuck that is two way data bindings (e.g. out of the box angularjs)
So yes, if your whole reasoning is "other people might use async and then I won't be able to use their code", then you'll be waiting indefinitely for the magical programming language that's both fully featured for your work and does not have any portion of the ecosystem implemented in async code.
For example, the common form of `await` calls implies cooperative multitasking and people will have a good reason to believe that no other tasks can't affect your code between two `await` calls. This is not generally true (e.g. Rust), but is indeed true for some languages like JS. Now consider two variants of JS, where both had `await` removed but one retains cooperative multitasking and another allows preemptitive tasks. They will necessarily demand different mental models, even though it is no longer syntactically distinguishable. I believe this distinction is important enough that they still have to be considered to have a function color, which is only uniform within a single language.
Zig's approach in comparison is often called "color-blind", because while it provides `async` and `await`, those keywords only change the return type to a promise (Zig term: async frame) and do not guarantee that it will do anything different. Instead, users are given the switch so that most libraries are expected to work equally well regardless of that switch. You can alternatively think this as follows: all Zig modules are implicitly parametrized via an implicit `io_mode` parameter, which affect the meaning of `async` and `await` and propagate to nested dependencies. There is definitely a color here, but it's no longer a function color because functions can no longer paint themselves. So I think it's reasonable to call this to have no function color.
[1] https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
This means, before async existed, any library doing IO had to be based on callbacks. Then came Promises, which are essentially glorified callbacks and then came async which can be seen syntax sugar for Promises.
So you will never see synchronous code that depends on an asynchronous result. The concept of sync code waiting for something just never existed in JavaScript. Instead you wake up your sync functions with Promise.then()-callbacks and that same mechanism bridges async functions.
It’s also very rare to have compute heavy sync code in JS so there is rarely any need to run it multi threaded.
No race conditions, no inter thread coordination, no concerns about all the weird shit that happens when doing multi threading.
Have you forgotten prompt() and friends?
When you're immutable, you can still delete or replace data.
From the point of view of the `async` block/function, it is blocking, but from the point of view of the thread executing that `async` block/function it is not.
> If I'm awaiting on two different results, I have to invoke them in parallel somehow, right?
No, the whole point of `async` is having concurrency (i.e. multiple tasks running and interleaving) without necessarily using parallelism for it. Take a look at the `join` macro in the `futures` crate for example, it allows you to `.await` two futures while allowing both of them to make progress (two successive `.await`s would force the first one to end before the second one can start) and without spawning dedicated threads for them.
Only if you limited async to only one thread on one core (why would you do that?) you could avoid that.
With the Spotify API before I have wanted to do concurrent API calls. One api call gives you a list of song IDs in a playlist, then you want to get all the song info for each ID. HTTP2 multiplexing would be much nicer than spawning 100 different http1 connections
Errrr..... no it's not. Your statement is flat out wrong. async is single threaded.
You're not really understanding async.
async is single threaded. If you want to maximise your cores then run multiple instances of a single threaded process using systemd or something, or use your application to launch multiple async threads.
You can, in python and probably in Rust, run an executor, which essentially is running a synchronous process in a thread to make some bit of synchronous work compatible with async, but that's not really ideal and it's certainly not the purpose of async.
In Go there's no function coloring because there are only async functions. That's why they don't get any special color, they are the only color. In Go you don't get to use sync functions, which creates problems e.g. when you need to use FFI, because the C ABI is the exact opposite and doesn't have function coloring because it only allows you to use sync functions.
Zig and Rust's async-generic initiative are a bit different in that they want to allow functions to be both sync and async at the same time. Ultimately there are still colors, but you don't have to choose one of them when you write a function. However IMO there are a lot of non-trivial problems to solve to get to that result.
Ultimately Go's approach work well enough, and usually better than other approaches, until you need to do FFI or you need to produce a binary without a runtime (e.g. if you need to program a microcontroller)
PS: it's also limited to argument and result types which we can serialize into a byte array. i.e. its utility is more limited than direct use of async APIs is, as those can accept any argument type and resolve to any type of value. That fine for the case of sqlite but it's not a general-purpose solution.
Now you do... have an incentive to write your own which is not async.
Rust's Tokio runtime, for example, is multi-threaded by default and makes progress on several tasks at the same time by using multiple threads.
The usual way it happens is that you write some code inside an async function that's straight line not suspending, and then later someone adds an await inside it. The await returns to the event loop, at which point an event arrive that you didn't expect. You now have control flow jumping to somewhere else completely, possibly changing state in a way that you didn't anticipate. When the original function resumes it sees something change concurrently.
I'd guess it could be an advantage for high concurrency applications that are CPU bound, but could be made IO bound by optimizing the userspace code. But OS threads are pretty efficient and you can have zillions of them, so the async upside is quite bounded, so this niche would seem smallish.
I agree that immutability is a tool. My issue with it is when you treat it as a rule.
If we call the go programming model async, the word has completely lost all meanings.
I've seen Heisenbugs where some random code calls a setter on an object in a shared memory cache. The setter call was for local logic - so immutable update would've saved the day. It had real world impact too: We ordered a rack with a European plug to an American data center (I think a human in the loop caught it thankfully).
Also, how often do you even use mutability really? Like .. for what? Logic is easier to express with expressions than a Rube Goldberg loop mutating state imo.
Having the ability to do so generally is tablestakes for being an intermediate professional programmer imo. In university, I had to draw diagrams explaining the state of the C stack and heap after each line of code. That's the same thing. And I was 19 lmao. It's not hard.
Maybe you're referring to space leaks? I've run into like 2 in my ten year Haskell career, and neither hit prod
I've actually seem more Go and Java space leaks/OoM bugs hit prod than Haskell - despite having fewer total years using those languages than Haskell! Nobody blamed the language for those though :/
These days async functions are also typically lazily evaluated via partial evaluation and the return continuation is not necessarily provided at the call site.
A sync function provides it's result via the normal return path.
For typical libraries, functions and structs are provided, and to the extent that these functions call functions provided by the user, they are generic over the async-ness of those functions. That's how the language-level async feature works, for library code that doesn't specify that it is async and doesn't specify that it would like to use a specific non-async calling convention for calling the user's callbacks.
Details which are meant to be ignored. When you use async/await constructs in various languages, you don't care about the fact that they are desugared into callback chains under the hood. You either do async/await in a language or you don't. That's what the concept of 'your function has a colour' means. If you want to change the meaning, OK but then you're talking about something else.
I suspect, given the real, actual measurements, the number of difficult to deal with bugs is pretty consistent between immutability and mutability. Actual measurements does not support claims of “easier to reason about”, or “reduced bugs”.
>how often do you use mutability
Whenever something should change and I don’t specifically need functionality that immutability might provide (literally 99.99999999% of every state change).
Immutability has some big advantages for pure logic, such as allowing containers to be treated as values the same as numbers. And efficient immutable data structures of all kinds are commonplace now.
I use async regularily for my clients, and I'm 100% sure that the usual async executors in Rust are multithreaded. I just ran gdb on an async program again, and, sure enough, the tokio async executor has 16 threads currently (that's just on a laptop with 16 cores).
async fn say_world() {
println!("world");
}
#[tokio::main]
async fn main() {
loop {
say_world().await;
}
}
(gdb) info threads
Id Target Id Frame
1 LWP 32716 "r1" 0x00007fdc401ab08d in ?? ()
2 LWP 329 "tokio-runtime-w" 0x00007fdc401ab08d in ?? ()
3 LWP 330 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
4 LWP 331 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
5 LWP 332 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
6 LWP 333 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
7 LWP 334 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
8 LWP 335 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
9 LWP 336 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
10 LWP 337 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
11 LWP 338 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
12 LWP 339 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
13 LWP 340 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
14 LWP 342 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
15 LWP 343 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
16 LWP 344 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
17 LWP 345 "tokio-runtime-w" 0x00007fdc401a8482 in ?? ()
Just try it out.Also, think about it. Using async in order to speed up I/O and then pin the async executor to just one core of your 200 cores on a server is not exactly a winning strategy.
>executor, which essentially is running a synchronous process in a thread to make some bit of synchronous work compatible with async
That's not what an executor is.
Also, the thing above is an example of parallelism, so even worse than concurrency. But even with an one-thread-async-executor you could still get concurrency problems with async.
>If you want to maximise your cores then run multiple instances of a single threaded process using systemd or something, or use your application to launch multiple async threads.
It is not 1995. Your idea would make scheduling even harder than it already was, and it would add massive memory overhead. If you are gonna do that, most of the time, just use synchronous processes to begin with--no need for async.