C# and Rust (via Tokio) both have M:N threading. They both use a work-stealing algorithm to map many tasks onto a finite thread pool. But you're correct that they are cooperative via async/await, not pre-emptive.
You can try this yourself: GODEBUG=asyncpreemptoff=1
Also platforms like Wasm still do Mx1 scheduling without async preemption, where Gosched is required at places.
E.g.: my "transpiled" SQLite driver takes special care to make sure long running SQL queries (and the busy handler) can be canceled with contexts even on platforms without async preemption.
I believe it’s more about robustness than performance in typical cases. Without pre-emption, there’s always a risk of one goroutine using disproportionate CPU time if it gets into an infinite (or just very long) loop without doing any IO.
I'm curious; using hardware threads is M logical threads preemptively scheduled on N physical cores. In what way does this not satisfy the original criteria?
They are still full OS threads: they have a full-size stack and have all the same overheads for context switching. Why would you think that a marketing term for a CPU feature is equivalent to an M:N scheduler?
A "hyperthread" can schedule work for two OS threads simultaneously on a single core. An M:N scheduler will schedule millions of green threads on as many cores/hardware threads as you give it (typically you'd give it all of them).
That is not true of preemptive green threading systems. Go/Erlang/Haskell all run native foreign code on its own threads, and manage all internal I/O with async schedulers that never block. They also preempt user code in tight loops.
Those are the only mature languages that have all of these features.
Not sure if purposefully but you left out java. It doesn't preempt a CPU hot loop, but arguably you don't really want to disturb the CPU there / the whole "green thread" model doesn't make much sense with that kind of workload. That's why you have ordinary threads as well.
That's why higher level languages are in an advantage. E.g. in java - outside of FFI - syscalls are basically "only within" standard library functions. So it was possible to make them virtual thread aware, e.g. park a virt thread, use something like io uring for the actual IO call on the JVM level and resume it when it returns, doing work on another virt thread in the meanwhile.
Before that, Go did preempt on function calls. Haskell preempts on memory allocation. There is no idiomatic Haskell code that loops without allocating, but it is possible. Go code that loops without function calls is probably a lot more common but still avoidable.
I wasn't comparing Go to every language ever, just the ones people are most likely to pick. In that group Go (and Elixir) have more unique concurrency models.
AFAIK, in the current implementation, Java's virtual threads yields only when they block (cooperative). But the spec allows a JVM to implement them as preemptive.
Other languages and their implementations of green threads usually have cooperative scheduling or M:1 mapping