Goroutines
Una goroutine è un'unità di esecuzione indipendente all'interno del processo corrente.
Generazione di goroutines
Quando un eseguibile Go viene lanciato, un primo processo di bootstrap inizializza l'ambiente di esecuzione del processo, creando le regioni di memoria necessarie e lanciando alcuni threads di esecuzione del sistema operativo:
- Il gestore di memoria
- Il garbage collector
- Lo schedulatore
Lo schedulatore genera la routine principale, il main.
Dall'interno del main possono venir lanciate routines aggiuntive, le goroutines col comando
go funzione
Ogni goroutine è gestita dallo schedulatore che la esegue a intervalli regolari e per una fetta di tempo.
Le goroutines non sono threads per se. Possono però essere allocate in threads di sistema diversi su diverse CPU. Di default vengono utilizzate tutte le CPU disponibili.
Comunicazione tra goroutines
Una goroutine può ricevere parametri iniziali dal main tramite la funzione di lancio, pio diventaindipendente e il main non le può più passare parametridirettamente.
La comunicazione tra goroutines e main (che è formalmente un'altra routine) e tra le routines stesse avviene tramite canali detti channels.
I channel sono primitive di bufferizzazione e sincronizzazione dati simili alle pipes di Unix, ma in ambiente concorrente.
I channel sono in memoria dinamica e vanno creati, gestiti e chiusi. Possono essere monodirezionali o bidirezionali, possono avere una capacità determinata di dati. Vi sono operatori di lettura e scrittura dei channel.
Terminazione di goroutines
Una goroutine può decidere di terminare col comando return
.
Quando è il main che compie un return, l'intero processo termina:
- sono distrutte tutte le goroutines
- sono eseguite eventuali funzioni differite
- viene disallocata tutta la memoria e tutte le risorse usate dal processo
- il processo termina nel sistema operativo
Una goroutine non può comandare ad un'altra goroutine di terminare, solo indicarne l'intenzione tramite channel.
Esempio
Il programma seguente illustra la differenza tra l'invocazione di una funzione direttamente dal main e tramite una goroutine
(220goroutines.go):
package main
import "fmt"
// Funzione che stampa una stringa 3 volte
func f(from string) {
for i := 0; i < 3; i++ {
fmt.Println(from, ":", i)
}
}
// Una goroutine è un thread eseguibile
func main() {
// Normale richiamo diretto di una funzione sincrona
// nello stesso thread del main
// E' sincrona
f("direct")
// Invocazione di una goroutine in un thread
// diverso dal main
// E' asincrona
go f("goroutine")
// Il thread viene generato, eseguito
// e distrutto al termine dell'esecuzione
// Goriutine come funzione anonima
// Notare il passaggio immediato dell'argomento
go func(msg string) {
fmt.Println(msg)
}("going")
// L'esecuzione arriva qui
var input string
// Acquisizione di un Return da input
// La stringa di input viene ignorata
// Notare che l'argomento è un indirizzo (= C)
fmt.Scanln(&input)
fmt.Println("done")
}
Le goroutines hanno sempre associata una funzione.
Questa può essere:
- già definita con nome altrove nel codice
- definita all'istante del lancio della goroutine e in tal caso non è necessario che abbia un nome, è anonima
Dato che goroutine diverse eseguono concorrentemente, non è definibile quale di loro è schedulata per prima.
Eventuale loro output non avviene necessariamente nella sequenza di lancio, e può differire tra invocazioni diverse del processo.
Wait Group
Prima di terminare il main deve dare tempo alle goroutines di terminare il proprio lavoro.
Questo si ottiene con una variabile di sincronizzazione di gruppo:
var wg sync.WaitGroup
Prima di lanciare una goroutine il main aggiunge una entry al gruppo:
wg.Add(1)
Una goroutine prima di terminare lo comunica al gruppo:
wg.Done()
e il main decrementa il numero delle goroutines attive.
Il main attende che tutte le goroutines abbiano terminato con:
wg.Wait()
poi può terminare a sua volta.
(221no-wg.go):
package main
import "fmt"
func main() {
fmt.Println("start")
go doSomething()
// Quando il programma termina
// tutte le goroutines sono terminate
fmt.Println("end")
}
func doSomething() {
fmt.Println("do something")
}
// Qualche volta lo scrive, qualche volta no
// Occorre che il main attenda la terminazione
// della goroutine
(222waitgroup.go):
package main
import (
"fmt"
"sync"
)
// Variabile di sincronizzazione di gruppo
var wg sync.WaitGroup
func main() {
fmt.Println("start")
wg.Add(1) // indica che apetteremo un programma
go doSomething()
fmt.Println("end")
wg.Wait() // attesa del completamento dei programmi
// uscita globale
}
func doSomething() {
fmt.Println("do something")
wg.Done() // segnala il completamento
}
Efficacia delle Goroutines
Quando determinate operazioni si possono parallelizzare, può essere molto conveniente usare goroutines.
Questo è tanto più vero quando vi siano più core computazionali
Non tutti i problemi possono però aumentare di efficienza con l’uso di goroutines.
Lo schedulatore ha uno overhead
- nella generazione di goroutines
- nello switch tra goroutines
(223mult-simple.go):
// Moltiplicazione con singolo thread
package main
import (
"fmt"
"time" )
func main() {
for n := 2; n <= 12; n++ {
timestable(n)
}
}
func timestable(x int) {
for i := 1; i <= 12; i++ {
fmt.Printf("%d x %d = %d\n", i, x, x*i)
// Introduce un ritardo voluto
time.Sleep(100 * time.Millisecond)
}
}
// Provare con:
// time go run 223mult-simple.go
(224mult-goroutines.go):
// moltiplicazione con più goroutines
package main
import "fmt"
import "sync"
import "time"
var wg sync.WaitGroup
func main() {
for n := 2; n <= 12; n++ {
wg.Add(1)
go timestable(n) // Calcola la tabellina dell’n
}
wg.Wait()
}
func timestable(x int) {
for i := 1; i <= 12; i++ {
fmt.Printf("%d x %d = %d\n", i, x, x*i)
time.Sleep(100 * time.Millisecond)
}
wg.Done()
}
// testarne la velocità