Sobes.tech
Middle+

Parla della pianificazione delle goroutine in Go.

sobes.tech AI

Risposta dell'AI

Il pianificatore di goroutine in Go utilizza il modello M:N, dove M goroutine sono mappate su N thread del sistema operativo. Cambia efficacemente le goroutine tra i thread, implementando multitasking senza preemption con punti di interruzione (punti di preemption).

Componenti principali del modello GPM:

  • G (Goroutine): Astrazione dell'unità di codice in esecuzione, thread leggero gestito dal pianificatore di Go.
  • P (Processor): Processore logico, rappresenta un contesto di coda locale di goroutine e risorse come mutex e pianificatore. La quantità di P di default è determinata dal numero di core logici del processore, ma può essere modificata tramite GOMAXPROCS.
  • M (Machine/Thread): Thread del sistema operativo che esegue il codice. M è associato a P e esegue goroutine dalla coda locale di P o dalla coda globale.

Modo di funzionamento:

  1. Viene creata una nuova goroutine e viene inserita nella coda locale di un P disponibile.
  2. M, associato a P, prende una goroutine dalla coda locale e la esegue.
  3. Se la coda locale è vuota, M tenta di rubare goroutine da altri P.
  4. Se P si blocca (ad esempio, durante una chiamata di sistema), l'M associato si disconnette, e un altro M si associa a quel P. M bloccato può essere riutilizzato quando la chiamata di sistema termina.
  5. Se una goroutine si blocca su un canale o mutex, viene messa in coda di attesa, e il pianificatore sceglie un'altra goroutine per eseguirla sullo stesso M.
  6. Il pianificatore esegue controlli periodici per scacciare goroutine che lavorano troppo a lungo, permettendo ad altre goroutine di ottenere tempo CPU.

I punti di preemption si verificano in luoghi specifici:

  • Chiamate di funzioni/methods.
  • Loop contenenti chiamate di funzioni.
  • Assegnazioni di memoria.
  • Primitive di sincronizzazione (canali, mutex).

Esempio di pianificazione:

package main

import (
	"fmt"
	"runtime"
	"time"
)

func worker(id int) {
	fmt.Printf("Goroutine %d inizia a lavorare\n", id)
	time.Sleep(time.Second) // Simulazione di lavoro
	fmt.Printf("Goroutine %d ha finito di lavorare\n", id)
}

func main() {
	runtime.GOMAXPROCS(2) // Imposta il numero di processori logici

	for i := 1; i <= 5; i++ {
		go worker(i) // Crea goroutine
	}

	time.Sleep(3 * time.Second) // Attende il completamento delle goroutine
	fmt.Println("Goroutine principale terminata")
}

In questo esempio, 5 goroutine sono pianificate su 2 processori logici. Il pianificatore di Go distribuirà queste goroutine tra i thread del sistema operativo disponibili, usando code locali e forse globali, oltre al meccanismo di "work stealing".

Vantaggi di questo approccio:

  • Scalabilità: Uso efficiente di sistemi multi-core.
  • Basso overhead: Goroutine leggere rispetto ai thread del sistema operativo.
  • Facilità d'uso: La pianificazione integrata nel linguaggio rende la programmazione parallela più accessibile.