Anton Sergeyev
Anton Sergeyev

Reputation: 979

How and why does golang scheduler recursively run goroutines in runtime/proc.go:execute?

I'm trying to break down how the Go scheduler works, and what I'm seeing in runtime/proc.go is:

  1. The schedule function calls execute to run a goroutine
  2. The comment for execute explicitly says this function never returns. It calls gogo function defined in one of the assembly files.
  3. The gogo function performs a jump to the address of the first instruction of a new goroutine.
  4. After this goroutine is completed, the schedule function is called again, so we're back to step 1.

If my understanding is correct, then how does this scheme avoid stack overflow? Does it have something to do with "infinite" stacks that automatically increase their size, or am I missing something here?

Upvotes: 4

Views: 1718

Answers (1)

Anton Sergeyev
Anton Sergeyev

Reputation: 979

So I spent some time researching the subject and can now try to answer my own question. The whole goroutine lifecycle turned out to be a bit more complex:

  1. New goroutines are created in a special goroutine called g0, which is kind of a main goroutine of a thread. Any call to go func changes the stack from whatever current goroutine it was called from to g0 (this is done in proc.go:newproc).
  2. When the goroutine is created (in proc.go:newproc1), its stack (and/or program counter, PC) is constructed in a way that it looks like it was called by goexit function. This is done to guarantee that when goroutine completes and returns, it is returned to goexit.
  3. When schedule is called and a goroutine is chosen to run, the execute function executes it (== jumps to its address via the gogo assembly function).
  4. After the goroutine has completed, it returns to goexit function, implemented in assembly.
  5. That assembly function calls proc.go:goexit1 (not sure why this extra step in assembly is needed).
  6. The goexit1 function changes current stack to g0. This is done with a call to mcall ("Machine thread call"), which executes whatever function is received in an argument. In this case the function supplied to mcall is goexit0.
  7. The mcall, implemented in assembly, jumps to the address of g0's stack frame (SP) and performs a CALL to goexit0.
  8. The goexit0 function is executed in the context of g0. It puts a completed goroutine on a list of free goroutines, and frees its stack if it was previously increased.
  9. Then goexit0 calls schedule again, which chooses a goroutine to run, so we get back to step 3.

So indeed there seems to be no recursion here. The scheduled goroutine itself never calls schedule: this is done by a special goroutine g0. I'm still not sure if I captured all the details though, so comments and additional answers are appreciated.

Upvotes: 6

Related Questions