What you're paying for with async/await is a state machine that describes the concurrent task, but that state machine can be incredibly wasteful in size due to the design of futures and the desugaring pass that converts async/await into the state machine.
That's why I said it's not "zero cost" in the loosest definition of the phrase - you can write a better implementation by hand.
That's exactly how async/await works, except that it translates to state machines under the hood which gives you great performance. No need to mess with threading models, at all.
Compared to what?
Doing epoll manually?
If you care enough, you generally should be able to outdo the reactor and state machines. Whether you should care enough is debatable.
Even just synchronizing on an atomic can thrash branch prediction and L1 caches both, let alone working your way through a task queue and interrupting program flow to do so.
But the moment somebody drops async into my codebase, yay, now I get to pay the cost.
- coding multi-frame logic in a straightforward way, which is when transforming a function into a suspendable state machine makes sense
- using more cores because you're CPU-bound, which is literally multithreading
Both cases can be covered by other approaches, though:
- submitting multi-frame logic as job parameters to a separate system (e.g., tweening)
- using data parallelism for CPU-intensive work
In any case you aren't writing sequential code, it's still concurrent code, and there's a trade-off between the writing simplicity of writing it as if it was sequential code, and the reading simplicity of having things written down explicitly.
This “write-time vs read-time” trade of is everywhere in programming BTW, that's also the difference between error-as-return-values and exception, or between dynamic typing and static one for instance.
Sure there's no reason to do that, because non-blocking syscalls are just better, but you can…
There is. When you write async functions, they get split into state machines and units of non-blocking work which need to be added and taken from work queues. None of this has to happen if you just spawn an OS thread and tell it "execute this function". No state machine, no work queue. It's literally just another sequential program that can do blocking I/O independently of your main thread.
If you insist on implementing a thread-based solution in exactly he same way that an async solution would, then yes they'll both pay the price of the convoluted runtime. The point is, there's no need to do that.