Gestione delle Goroutine
Le Goroutine sono leggere, ma non sono gratuite: almeno consumano memoria per lo stack e la pianificazione della CPU. Anche se questi costi sono piccoli per l'uso delle Goroutine, generarle in grandi quantità senza un ciclo di vita controllato può portare a gravi problemi di prestazioni. Le Goroutine con cicli di vita non gestiti possono anche causare altri problemi, come prevenire che gli oggetti inutilizzati vengano raccolti dal garbage collector e trattenere risorse inutilizzate.
Pertanto, non lasciare fuoriuscire Goroutine nel tuo codice. Utilizza go.uber.org/goleak per testare le fuoriuscite di Goroutine all'interno di un pacchetto che potrebbe generare Goroutine.
In generale, ogni Goroutine deve:
- Avere un tempo di arresto prevedibile; o
- Avere un modo per segnalare alla Goroutine che deve fermarsi.
In entrambi i casi, deve esserci un modo per bloccare il codice e attendere che la Goroutine completi.
Ad esempio:
Non consigliato:
go func() {
for {
flush()
time.Sleep(delay)
}
}()
// Non c'è modo di fermare questa Goroutine. Continuerà all'infinito finché l'applicazione non viene chiusa.
Consigliato:
var (
stop = make(chan struct{}) // Segnala alla Goroutine di fermarsi
done = make(chan struct{}) // Segnala che la Goroutine è uscita
)
go func() {
defer close(done)
ticker := time.NewTicker(delay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
flush()
case <-stop:
return
}
}
}()
// ... altro codice
close(stop) // Indica che la Goroutine dovrebbe fermarsi
<-done // e attendere che esca
// È possibile attendere che questa Goroutine esca con close(stop), e possiamo attendere che esca <-done.
Attesa dell'uscita delle Goroutine
Quando si ottiene una Goroutine generata dal sistema, deve esserci un modo per attendere che la Goroutine esca. Ci sono due metodi comuni per raggiungere questo:
- Utilizzando
sync.WaitGroup
. Utilizza questo metodo quando si attendono più Goroutine.
var wg sync.WaitGroup
for i := 0; i < num; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// ...
}()
}
wg.Wait()
- Aggiungendo un altro
chan struct{}
, e chiudendolo dopo che la Goroutine è completata. Utilizza questo metodo se c'è solo una Goroutine.
done := make(chan struct{})
go func() {
defer close(done)
// ...
}()
// Per attendere che la Goroutine finisca:
<-done
La funzione init()
non dovrebbe creare Goroutine. Inoltre, consulta Evitare di utilizzare init().
Se un pacchetto richiede una Goroutine in background, deve esporre un oggetto responsabile della gestione del ciclo di vita della Goroutine. Questo oggetto deve fornire un metodo (Close
, Stop
, Shutdown
, ecc.) per indicare la terminazione della Goroutine in background e attendere la sua uscita.
Approccio non consigliato:
func init() {
go doWork()
}
func doWork() {
for {
// ...
}
}
// Quando l'utente esporta questo pacchetto, viene generata incondizionatamente una Goroutine in background. Gli utenti non possono controllare la Goroutine o fermarla.
Approccio consigliato:
type Worker struct{ /* ... */ }
func NewWorker(...) *Worker {
w := &Worker{
stop: make(chan struct{}),
done: make(chan struct{}),
// ...
}
go w.doWork()
}
func (w *Worker) doWork() {
defer close(w.done)
for {
// ...
select {
case <-w.stop:
return
}
}
}
// Shutdown dice al worker di fermarsi
// e attende che completi.
func (w *Worker) Shutdown() {
close(w.stop)
<-w.done
}
Genera il worker solo quando richiesto dall'utente e fornisce un metodo per fermare il worker in modo che l'utente possa rilasciare le risorse utilizzate dal worker.
Nota che se il worker gestisce più Goroutine, dovrebbe essere utilizzato un WaitGroup.