Is Tokio slow, or is it just being compared incorrectly?

100 points last_account_promise

Hi folks! I'm struggling to understand some concepts I recently learned about writing performant async rust, can someone help me confirm that I'm understanding these concepts correctly?

Often, performance comparisons are made against Tokio and Go's Goroutines, and Tokio doesn't come up on top. This thread comes to mind. Often, feedback on those threads is: there is something wrong with the way the Rust code was written that's causing a performance bottleneck, leading to the Go code being faster. This feeds into the perception that writing "good" (or, at least, "performant") Rust code is more difficult than in Go.

Here's where I'm confused, though. Aren't Tokio and Go's goroutines fundamentally different things? My understanding is that Tokio is a stackless coroutine library, meaning that unless you yield at the right times, you're not getting proper async/parallel execution (you're "blocking the executor"). On the flipside, Go's Goroutines are stackful coroutines, which means they can remember where they left off, and they don't need to explicitly yield. Thus, Tokio should have a higher "maximum performance" (because stackful coroutines have an overhead), but should be harder to get right (because you have to explicitly yield).

If I'm correct about this, then a true apples-to-apples comparison would be a stackful coroutine library in Rust. It seems like May is the most popular one of these.

I guess my questions are:

  1. Am I understanding all of this correctly?
  2. Why are stackful coroutines not more popular in Rust, especially given the perception that "Rust is super hard"?

EDIT: I will update my most up-to-date understanding here.

It seems there's another axis of differentiation between Tokio/Goroutines: Goroutines are preemptively scheduled, and Tokio coroutines are cooperatively scheduled. This is a distinction that allows Goroutines to be written with a lower regard for optimization. It seems in my original post, I said stackful/stackless, but it was really cooperative/preemptive scheduling that causes this effect. (EDIT 2: see below for another reason)

It's unclear to me what the stackful/stackless distinction contributes to the differences experienced by users of Tokio/Goroutines.


EDIT 2: There is another reason why Go is easier: all IO in Go is non-blocking by default. Although Tokio has non-blocking versions of IO, the programmer has to remember to not accidentally call the std blocking version.