Le goroutine sono thread leggeri forniti dall'architettura di Go fin dalle prime versioni per raggiungere un'efficace concorrenza. Storicamente, l'idea di un lightweight-thread è emersa come un tentativo di superare il costo dei thread di sistema, oltre a rispondere all'alta richiesta di applicazioni server scalabili. Go è stato progettato fin dall'inizio come un linguaggio per sistemi server e di rete, dove milioni di attività devono essere elaborate in parallelo.
Problema: La concorrenza può rapidamente portare a race conditions, deadlock e aumento dei consumi di memoria se non si controlla il ciclo di vita delle goroutine, non si considera la loro pianificazione e non si gestisce la loro terminazione.
Soluzione: Le goroutine vengono avviate mediante la parola chiave go. Il lavoro delle goroutine è pianificato dallo scheduler di Go, che utilizza un modello M:N (M thread di sistema gestiscono N goroutine del linguaggio Go). Per gestire il ciclo di vita si utilizzano canali, WaitGroup, context e il controllo sulla chiusura dei canali.
Esempio di codice:
package main import ("fmt"; "time") func worker(id int) { fmt.Printf("Worker %d started\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id) } func main() { for i := 1; i <= 3; i++ { go worker(i) } time.Sleep(2 * time.Second) }
Caratteristiche chiave:
Se non si attende esplicitamente una goroutine in main, verrà sempre eseguita?
No, l'esecuzione di main termina — il processo si concluderà indipendentemente dallo stato delle goroutine figlie, e non tutte le attività saranno completate.
L'avvio di go func(...) da un ciclo garantisce che ogni goroutine ottenga il proprio valore delle variabili del ciclo?
No, si verifica un problema di cattura della variabile del ciclo, le goroutine potrebbero lavorare con lo stesso valore di slice/variabile. È necessario utilizzare una copia della variabile, ad esempio, passando come argomento:
for i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i) }
Può una goroutine bloccare lo scheduler di Go e impedire l'esecuzione delle altre?
Sì, se in essa si avvia un ciclo infinito o molto pesante senza punti di switch (ad esempio, senza chiamate di funzione temporale o yield), può trattenere il thread di sistema — anche se ciò contrasta con l'ideologia di Go riguardo alla "multitasking cooperativa". Ad esempio, una funzione pesante senza blocchi:
func busy() { for { // Nessuna attesa o chiamate bloccanti } }
In un microservizio si avvia periodicamente una goroutine per leggere dal database, ma si dimentica di terminarla in caso di annullamento della richiesta. Di conseguenza, rimangono "appese" goroutine che nel tempo portano al consumo di tutta la memoria RAM.
Vantaggi:
Svantaggi:
Si utilizza il context per controllare l'annullamento delle attività, WaitGroup — per gestire la terminazione di tutte le goroutine prima dell'arresto dell'applicazione, e canali — per una corretta trasmissione dei dati tra gli esecutori.
Vantaggi:
Svantaggi: