Outside of Embassy in embedded, tokio is the only realistic choice though, because it is likely that any third party async crate has a dependency on it already. Yes, smol, monio, glommio etc exist, but they are marginalised (and as far as I can tell they don't really help that much with mixed IO / compute workloads).
In fact, async/await in Rust falls apart with a mixed IO / compute workload since scheduling is cooperative. As soon as you want preemption (most of the time for what I do), it is not the right choice.
Seeing how embassy (embedded async rust) handles preemption reinforces this: it uses a separate scheduler per preemption level. This works fine, but is a bit clunky. Basically you are at this point just using async to help write a state machine per preemptive thread, which can be useful for some code patterns (in particular those common in embedded, where you are often waiting for IO). But to talk between threads you are back to channels, mutexes etc.
How do you properly mix IO/compute (in any language)? In Rust what I’ve done in the past is have two Tokio runtimes, one for IO and one for compute. I know you can also use Rayon but the abstractions are not always flexible/convenient enough.
But in either case the boundary between the two types of async work is never easy to cross.
Yes I do agree. It's not that Tokio has taken over just due to community momentum. There's a certain subtle lock-in that happens due to how the Rust type system and dependency system work together. It's a hard problem to address.
Regarding mixed IO/compute and preemptive scheduling, well that's what threads are. They are not as lightweight, but they are there and they are quite ergonomic to use in Rust.
I could even imagine an async executor that simply starts a thread per task, so that you can keep the nice syntax, but that's obviously not ideal from a performance standpoint. Tokio already dispatches tasks to a thread pool, it is not completely cooperative, it's the sensible way to do this. And there's always tokio::spawn_blocking too.
PS: Actually, it is underrated how well designed classic threading is in Rust. It was just going out of vogue when they did it, but they addressed so much of the complexity that was around for years in Java, C++ and the like. They did actually mostly achieve the holy grail of fearless concurrency and with a more ergonomic design.