This is a great example in Node on useful combinators that with async await make it easy to express parallel programming concepts with familiar tools. No manual IPC, no fork/join child PID/thread ID handling, etc.
https://github.com/sindresorhus/promise-fun
The same abstractions (or many of them) exist in Rust, but I think the above is illustrative of the ways we can combine async object returning functions and then use await to hide the complexity of the state machines needed to drive them.
That this abstraction that makes code easy to read and write also performs better is the icing on the cake. The former prevents bugs and keeps code quality high, and that is worth much more.
Rust is not Javascript. Using threads is actually a lot simpler in Rust than async/await.
So, code potentially laden with use after frees, double frees, shared and mutable data, and so on.
No offense to you, but I would be leery of trusting that code in any languages except a handful. Certainly not C/C++, and if it were written in Rust, I would hope it would use a thread combinator library and channels.
Also threads are definitely not easy to use in all languages. E.g. C++ gives you very little help (no channels for example), and JavaScript makes starting threads difficult and moving/sharing memory is limited to primitive arrays.
- A. Async/await - compiler saves and resumes functions.
- B. Message based - Golang, Erlang, threads with messaging.
With category A, I can use my IDE to jump to every function that is called and easily follow the computation.
With category B, all of these connections happen at runtime with messages.
When you have a tree of tasks all which may save/resume many times, async/await it easier to understand than launching a thread per IO event.
A. Implicit messaging using the languages function syntax (async/await).
B. Direct messaging using a message passing feature of the runtime (Erlang, Golang)
Note: I mean "messaging" in the context of a single OS process, that possibly has many threads (so within a single language runtime).
Async/await is still implicit messaging, but it appears like a regular function call - which in my opinion is easier to understand. Using function args/return for input/output is something every developer already knows.
In contrast, Erlang and Golang require you to use some type of messaging feature in addition to functions.
> A useful way to think of Go and Erlang is that they automatically and transparently insert async/await each time you call a function that performs I/O
The part they are missing from async/await is the ability to easily get return values without messaging, and do this recursively for a large tree of functions.
E.g. getting a return value from `go x()` requires messaging, but with async/await you could do `const p = x(); const ret = (await p); // return value received at a later time with no messaging.`
Both of them will require you to create some type of messaging topology to return the values (which makes your program a mixture of (regular functions + messaging features) vs async/awaits "everything looks like a function").
No, they do not. In Elixir for example if I call:
bytes = File.read!("filename.txt")
`bytes` will have the data returned from the function call immediately, with no need for message passing or awaiting the result. Under the hood, it is still asynchronous evented I/O. If I want to explicitly await for flow control reasons (await all of or one of multiple events) that is available in the stdlib in the `Task` module. E.g. t1 = Task.async(fn -> do_this_thing() end)
t2 = Task.async(fn -> do_this_other_thing() end)
Task.await_many([t1, t2])
You can accomplish most things without ever calling send/receive or writing your own gen_server etc.I think the key issue is that the inputs and outputs are disconnected in the static program text (and only connected dynamically at runtime).
Two contexts that matter for understanding how a system transitions between states are:
1. Program editing/reading.
2. Runtime.
I think AA is superior for understanding the system as a whole in both these contexts, because at edit time the IDE jump to def/show all usages allows you to understand every function that will be called, and at runtime you can get a stack trace to understand where the current function came from, and where it is going.
With message passing runtimes, both 1 and 2 require extra mental models on the part of the programmer, because they also need to understand the network topology (which either is not possible statically, or requires extra tooling on top of functions).
Message passing breaks down your system into CSP's, which makes it easy to understand each sync process, but hard to understand the whole system, as the same program-writing-process that allowed you to break down your components is working against you when you need to put them together again to understand the whole system.
I could be wrong as I have not used modern IDE's or debugging tools with message passing runtimes lately.
Last time I used Erlang (pre-Elixir), the `bytes` example would require you to set up a request/response with a blocking `receive`.
What other reason were you thinking of?