As a C++ engineer with 20+ years of experience, I recently had an employer project migrate to C++20 and shortly then C++23. We started using coroutines. Oh goodness they're fun. The simple things are indeed simple and easy.
And actually fairly easy to get wrong, too. Way too easy. I might know how coroutines work but goodness it's been difficult training the rest of the team.
It's when you start to get true asynchronicity with multiple jobs running concurrently and each job might have a different workflow... well all of the synchronization and waiting for multiple jobs doesn't quite exist in the standard yet. So the standard provides the language tools but third party libraries provide the actual functionality -- to various degrees of success. We use boost asio's awaitable and there's clearly some warts and even gaps in functionality that we've had to work around.
I like early adopting many things. Adopting coroutines, even in C++23, was perhaps a little too-early.
I really wish my employer would give me time (say... 2 years) to clean up a ton of things not just in our codebase but also in the libraries we use and even propose fixes to the standard itself.
Huh? Apart from C++?
So much time is spent on async because it's important and most other languages have ignored it for a long time.
Ever tried libuv in C? That was just a nightmare to work with, callbacks within callbacks within callbacks.
Rust's approach to async is the best I've seen so far in any language, even high level languages like javascript that depend on async to even function.
I'm not a Rust programmer but have used async/await in both Python and C#. I've also written concurrent code in Erlang. I'd choose the Erlang approach over async/await every time. One concurrency primitive - the process - and an ergonomic, coherent set of supporting features (message passing, supervisors). No function colours and all the baggage that comes with that. Less well discussed but no less important: it puts concurrency decisions in the hands of the function caller, not the implementer.
I understand Rust's focus on zero cost abstractions and, whilst I wouldn't pretend to understand the innards and consequences, get why green threads might not be compatible with that. OTOH that restriction doesn't hold for languages with a runtime like C# and Python. I'm increasingly convinced the compromises of async/await make it a poor language design for the concurrency problem when the language has a runtime.
Perhaps we'll see some comparitive studies now Java has green threads. Erlang is different from C#/Python in many ways so straight comparison is hard. Java is much closer to C# so should be a much better basis for comparison.
It's not "hurr durr don't block thread" which is where a lot of developers stop reading at, at their own loss. And then come asking about green threads in github issues in a classic X Y problem fashion.
It is a paradigm where method calls represent an asynchronously produced result, a Task/Promise. Therefore, if all you do is always just await all the results right after calling async methods, you have not used the other 80% of the features.
Task<T> is about composing, chaining and interleaving the tasks, sometimes hundreds or thousands at a time, to achieve (sometimes massively) parallel and concurrent execution of application logic. And we are blessed with C# making it as easy as it gets.
In addition, there are single-threaded and thread-per-core executors with different future bounds. It is that Tokio puts more requirements on Send and Sync because it is a proper implementation with worker-per-core (configurable to be otherwise) + work stealing.
> Task<T> is about composing, chaining and interleaving the tasks, sometimes hundreds or thousands at a time, to achieve (sometimes massively) parallel and concurrent execution of application logic.
Replace Task<T> with Erlang processes and the statement holds. Except without coloured functions; without the async decision being in the wrong place; without codebases where dual versions of functions proliferate (DoSomething() / DoSomethingAsync()).
At its heart, concurrency is about being able to express multiple sequences of actions such that there's no undesired interaction between them when running. Thread blocking is one form of undesired interaction. So "hurr durr don't block thread" does matter even if it's not the only thing.
var data1 = service1.GetData();
var data2 = service2.GetAnotherData();
var aggregate1 = Aggregate(await data1);
var result = Handle(await aggregate1, await data2);
No need to deal with writing three lines per each operation to just schedule it in a "fork" way like in Java. In "colorless" (which is always a lie) async runtimes you have to go out of your way to make it concurrent. Perhaps Erlang process isn't coarse grained abstraction as you say, but there are multiple aspects that make Erlang problematic. And again, I will not tire of repeating it - the article about function coloring is actively harmful to the industry and is leading hundreds of developers with concurrency knowledge gaps assume that they need to avoid Task/Future-based code like plague when it is actually the best abstraction we have today for massively concurrent processes (if it was badly designed in whatever language of your choice - sorry).In addition, because Task<T> is a thread-safe object in C#, you can apply all kinds of transformations and data chaining with LINQ on collections/sequences of those, or even together with parallel LINQ and tasks at the same time. Your simple average code will easily scale to all CPU cores if it does not have interdependencies/contention (a lot of LOB codebases don't, it's all straight line up to a DB or a third-party call(s)).
And last but not least, all BEAM-based languages are comparatively slow (hard performance ceiling is always imposed if you don't pay with static typing and full JIT/AOT) and unfortunately suffer from high heap footprint, even compared to the more throughput-focused GC modes in .NET and GC implementations in JVM. But no, developers are insistent on parroting quotes said 10 to 15 years ago instead of at least attempting to assess technologies on their merits of today.
No, colourless isn't a lie. Erlang doesn't have two types of function (sync and async). C#/Python now do. As a function/method implementer in those languages, for every single function implementation, I am faced with the following:
1. My function has to be async, because someone, somewhere, in the call chain of functions I want to invoke, decided to make their function async. So I have to deal with the downsides (lower performance, debugging complexity) whether I need the upsides or not.
2. I need to decide whether to make my function async because the decision hasn't been taken out my hands somewhere down the call stack. If I'm writing a library function that means I'm now having to judge how callers will use my function. If I decide async, then I've imposed constraints on the caller as per #1.
3. I need to implement sync & async versions of my function so as not to constrain the choices of my callers.
That emerges because of (1) the asymmetric constraint imposed by async/await and (2) the requirement for function implementers to make the decision, not callers. A sync function can't call an async one. That's the basis of colouring. It's not a lie.
> the article about function coloring is actively harmful to the industry
No, the article about colouring very clearly explains the asymmetric nature of sync/async and the limitation it imposes. Its use of colour as a metaphor very clearly illustrates the issue. Sure, there's a risk that some people won't fully read and understand it - and then just parrot "yeah, coloured is bad". That's not the fault of the article though.
There's nothing inherently wrong with Futures as a concurrency construct: it's essentially enabling cooperative multitasking. The issue is that async/await as an implementation causes codebase bifurcation.
> all BEAM-based languages are comparatively slow
Emphasis on "comparative". I don't disagree that in certain dimensions - notably raw compute performance and memory usage - BEAM languages are comfortably down the benchmark tables compared to C#. That doesn't always translate to real world practicality though (and even throughput is getting better given the active JIT work).
> developers are insistent on parroting quotes said 10 to 15 years ago instead of at least attempting to assess technologies on their merits of today.
Futures have merit per above. Async/await brings syntactic convenience which, in isolation, is an improvement. But with it comes significant cost. Cost that isn't there with green threads and isn't intrinsic to Futures either. You can't just sweep those limitations under the carpet by promulgating the hubris that the arguments are old.
You'd do it like this: https://github.com/boostorg/cobalt or like this: https://github.com/danvratil/qcoro
> Async style language features are a compromise between your execution model being natively compatible with the 1:1 C ABI, C standard library, and C runtime and a M:N execution model. C++ async suffers from the same issues, except it’s not as strict in terms of lifetime safety (not a good thing). The cost for the native compatibility with the C/system runtime is the “function coloring” problem.
> Go, Haskell, and I assume Erlang make the other compromise. They eschew the C ABI and runtime completely and implement their own standard library. All code ends up being color-clean. The cost is that integrating with code outside their ecosystem is complex and slow.
And the "bit more wasteful" part is a non-starter because people want to use async in embedded contexts.
> For those who don’t know, there was a big debate whether the await operator in Rust should be a prefix operator (as it is in other languages) or a postfix operator (as it ultimately was). This attracted an inordinate amount of attention - over 1000 comments. The way it played out was that almost everyone on the language team had reached a consensus that the operator should be postfix, but I was the lone hold out. At this point, it was clear that no new argument was going to appear, and no one was going to change their mind. I allowed this state of affairs to linger for several months. I regret this decision of mine. It was clear that there was no way to ship except for me to yield to the majority, and yet I didn’t for some time. In doing so, I allowed the situation to spiral with more and more “community feedback” reiterating the same points that had already been made, burning everyone out but especially me.
The whole point of Rust is to not be a bit more wasteful
The tl;dr is: "The previous async approach ended up not working, and had to be removed. It's currently an incredibly hard problem with no clear rodemap. The plan is to get there eventually."
As far as I know at least part of the idea was to eliminate the function coloring problem by letting the compiler do some nifty compile-time deductions. This had some issues (I don't know if this is still planned, it seems like the kind of thing that should not work in practice). Additionally, there were all sorts of hard technical issues with LLVM, debugging, etc.
I recommend checking the issue tracker, eg. https://github.com/ziglang/zig/issues/6025
I personally don't understand the domain well enough at all, but honestly, I feel like (if possible) Zig should try to double down on its allocator approach.
Instead of trying to use some compile-time deduction magic explicitly pass around an "async runtime/executor" struct which you explicitly have to interact with. Why not?
That being said, even if we ignore wastefulness, have you tried async programming in OCaml or in Haskell? You immediately enter a CPS/monadic nightmare that makes programming way more complicated, debugging extremely hard and doesn't deal too well with errors.
These are the hard async problems that Rust is attempting to solve. Performance isn't the main blocker here.