I'm not sure what you mean by ‘more threads than I had cores’, though. Unless you tell it otherwise, Tokio will default to one thread per core on the machine.
Meanwhile the async _code_ itself is just a new(ish), lower-level way of writing code that lets you peek under an abstraction. Traditional ‘blocking’ I/O tries to pretend that I/O is an active, sequential process like a normal function call, and then the OS is responsible for providing that abstraction by in fact pausing your whole process until the async event you're waiting on occurs. That's a pretty nice high-level abstraction in a lot of cases, but sometimes you want to take advantage of those extra cycles. Async code is a bit more powerful and ‘closer to the metal’ in that it exposes to your code which operations are going to result in your code being suspended, and so gives you an opportunity to do something else while you wait.
Of course if you're not spending a lot of time doing I/O then the performance improvements probably aren't worth dropping the nice high-level abstraction — if you're barely doing I/O then it doesn't matter if it's not ‘really’ a function call! But even so async functions can provide a nice way of writing things that are kind of like function calls but might not return immediately. For example, request-response–style communication with other threads.
Yes! Mutexes are much nicer in Rust than a lot of languages, but they're still much too low-level for most use-cases. Ironically Lindsey Kuper was an early contributor to the Rust project and IIRC at roughly the same time started talking about LVars [1]. But we still ended up with mutexes as the primary concurrency mechanism in Rust.
Async mutexes in Rust have so many footguns that I've started to consider them a code smell. See for example the one the Oxide project ran into [1]. IME there are relatively few cases where it makes sense to want to await a mutex asynchronously, and approximately none where it makes sense to hold a mutex over a yield point, which is why a lot of people turn to async mutexes despite advice to the contrary [2]. They are essentially incompatible with structured concurrency, but Rust async in general really wants to be structured in order to be able to play nicely with the borrow checker.
`shadow-rs` [3] bears mentioning as a prebuilt way to do some of the build info collection mentioned later in the post.
[1]: https://rfd.shared.oxide.computer/rfd/0609 [2]: https://docs.rs/tokio/latest/tokio/sync/struct.Mutex.html#wh... [3]: https://docs.rs/shadow-rs/latest/shadow_rs/