Sobes.tech
Middle

Tell about goroutine scheduling in Go.

sobes.tech AI

Answer from AI

The Go scheduler uses a multi-threaded M:N model, where M goroutines are mapped onto N OS threads. The scheduler's work is based on the GMP model (Goroutine, Machine, P).

  • G (Goroutine): A lightweight thread of execution. Created with the go keyword.
  • M (Machine): An OS thread. Executes goroutine code. A hot thread, with blocking system calls executed on a separate M.
  • P (Processor): A local queue of goroutines to which an M is bound. The number of P defaults to the number of logical CPU cores and is controlled by the GOMAXPROCS variable.

Working principle:

  1. When a goroutine needs to be run, it is placed in the local P queue.
  2. The M bound to P takes a goroutine from the queue and starts executing it.
  3. If the local P queue is empty, M can "steal" a goroutine from another P's queue.
  4. If a goroutine blocks (e.g., during a system call), the current M detaches from P, and P is bound to another available M or a new M is created. The blocked goroutine remains on the blocked M until the blocking operation completes.
  5. When the blocking operation finishes, the goroutine is returned to another P's queue or the current P if it is free.
  6. The scheduler periodically (every 10 ms) checks goroutines for execution longer than 10 ms for preemption.

Advantages of this approach:

  • Low overhead for creating and switching goroutines (much less than OS threads).
  • Efficient CPU resource utilization through load balancing among P.
  • Asymptotically better performance compared to the 1:1 model (each goroutine is an OS thread).

Scheduling models:

  • Runqueue: Each P has a local queue of ready-to-run goroutines.
  • Global Runqueue: A global queue for "stolen" or ready-to-run goroutines when local queues are empty. Used less frequently.

The Go scheduler is not strictly preemptive. Context switching occurs:

  • During blocking operations (I/O, mutexes, channels).
  • When calling functions that may lead to a context switch (e.g., runtime calls).
  • During "voluntary yield" via runtime.Gosched().
  • During time-based preemption (scheduled from Go 1.14).
// Example of using goroutines
package main

import (
	"fmt"
	"time"
)

func worker(id int) {
	fmt.Printf("Worker %d started\n", id)
	time.Sleep(time.Second) // Simulate work
	fmt.Printf("Worker %d finished\n", id)
}

func main() {
	for i := 1; i <= 5; i++ {
		go worker(i) // Create and start goroutine
	}

	time.Sleep(2 * time.Second) // Wait for all goroutines to finish
	fmt.Println("All workers finished")
}