Sobes.tech
Middle+

What is runtime in Go and how does the scheduler work?

sobes.tech AI

Answer from AI

Runtime in Go is the execution environment that includes:

  • Scheduler: manages goroutines and their execution on system threads.
  • Garbage collector: automatically manages memory.
  • System calls: wrappers over OS system calls.
  • Network I/O: non-blocking network interaction.

The Go scheduler implements an M:N model, where M goroutines are executed on N system threads. It uses a three-component G-P-M model:

  • G (Goroutine): lightweight thread of execution managed by the Go scheduler.
  • P (Processor): logical processor representing the context needed to run goroutines (goroutine queue, stack cache). The default number of P is runtime.NumCPU().
  • M (Machine): system thread on which code runs.

Scheduler working principle:

  1. Creating goroutines: New goroutines are created using the go keyword.
  2. Queuing: The created goroutine is placed in a local P queue or a global queue if the local one is full.
  3. Execution: M is bound to P and selects a goroutine from its local queue (or global if local is empty). M executes the goroutine's code.
  4. Context switching: Occurs when a goroutine blocks (e.g., on a system call, waiting for a mutex, I/O operation) or when the scheduler decides to move a goroutine (e.g., after a time slice).
    • Blocking M: If a goroutine makes a blocking system call, the current M is detached from P, and the scheduler finds or creates a new M for that P. The blocked M is returned to the pool when the system call completes.
    • Preemption: In older Go versions, preemption was cooperative (goroutine had to explicitly yield). Modern versions (from 1.14) implement asynchronous preemption based on OS signals, allowing interruption of goroutines stuck in non-blocking code.
  5. Work stealing: If a P runs out of goroutines in its local queue, it can "steal" goroutines from other P's or from the global queue.

Advantages of the Go scheduler:

  • High performance: efficiently utilizes multi-core processors.
  • Lightweight goroutines: allows creating millions of concurrently running "threads".
  • Non-blocking I/O: simplifies writing high-performance network applications.
  • Ease of use: developers do not need to manage threads manually.

Example of creating a goroutine:

package main

import (
	"fmt"
	"time"
)

func worker() {
	fmt.Println("Goroutine is running")
}

func main() {
	go worker() // Creating a new goroutine
	time.Sleep(time.Second) // Waiting for the goroutine to finish
	fmt.Println("Main goroutine finished")
}