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:
- Viene creata una nuova goroutine e viene inserita nella coda locale di un P disponibile.
- M, associato a P, prende una goroutine dalla coda locale e la esegue.
- Se la coda locale è vuota, M tenta di rubare goroutine da altri P.
- 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.
- 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.
- 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.