Coroutines are nice as they allow trivially writing an app which scales to millions of concurrent operations. Rust is nice as it lets us write code for every occasion, async is awful as it poisons the entire ecosystem. The same result could have been achieved with a few tactically inserted yield statements for I/O and locking which are pretty much the only times that async or coroutines help.
It doesn't have to be one or the other. Go uses coroutines and multiplexes them onto multiple OS-level threads (usually as many as there are cores).
Your application has n threads to begin with, if you use async, these will be utilized to schedule tasks. You're basically trusting your language to properly pause and restart the procedures.
If you spawn new threads, these will be managed by your kernel. If that kernel is Linux, that it already has a pretty good algorithm to pause and restart them, so you're not really wasting a lot of resources.
So I believe there are effectively two differences: threads can scale up to the limit of your os/kernel and hardware. Async can scale up to the level of the threads your language spawns to handle these tasks. Threads trust the kernel/OS to prioritize workloads, async trusts the language
Thread context switches are pretty expensive. If you use an async runtime such as tokio, all your tasks will be spawned on a fixed number of threads and you won't have any context switches on a task switch.
Thread context switches are pretty expensive.
Opinions are pretty divided on this one these days. There was a time when this was very much true but there is some evidence that it is no longer, practically speaking, an issue except in extreme cases. You do pay a cost but it's not immediately obvious that the cost is material. I reach for a threadpool first and async when it becomes clear that I need the extra performance. 99% of the time the threadpool is more than good enough.The interesting thing about async (especially async-await) is that it is a fluent way to write a program that waits for events (e.g. I/O), without resorting to inefficient designs like thread-per-client in order to achieve concurrency.
Software (even in C) has made use of this concept for a long time through poll() and friends; async-await is just a higher level abstraction over the same concept.
I couldn't disagree stronger on that one. Abstraction is generally good, but this becomes a purely philosophical discussion as soon as you ignore the actual implementation when talking about the differences of how something actuals behaves and should be used. And philosophical discussions like that have their place but are imo pointless in the context of what should be used, when. They're generally better placed in a context of deciding wherever you want to implement an abstraction
I do fundamentally agree with the rest of your comment however. that's what I meant with trusting your language to prioritize workloads vs your OS.
FYI, Rust (the language) has no concept of prioritising workloads; there is no async runtime in the language.
Even with any of the async runtimes, workloads (i.e. tasks doing work) are prioritised by the OS. It's just threads executing code. Async runtimes are primarily concerned with the gaps between work, like waiting for I/O or other events.