← Back to context

Comment by dlisboa

20 hours ago

No. Preemptive scheduling plus M:N mapping combination that Go has is not common in other major implementations.

Other languages and their implementations of green threads usually have cooperative scheduling or M:1 mapping

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.

  • Very curious how much Go wins by this. How worse would typical Go programs run with pre-emption disabled?

    • 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).

Haskell had preemptive M:N green-threading before Go was invented.

I believe Go didn't originally have it, and added it in 2020, 14 years after Haskell.

  • 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.

Java's virtual threads are M:N.

  • >> Preemptive scheduling plus M:N mapping

    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.

    • Well, they do only yield on block and whatnot, but that's very much different from adding manual async/await keywords, I would say.