Gestion des Goroutines

Les goroutines sont légères, mais elles ne sont pas gratuites : au minimum, elles consomment de la mémoire pour la pile et la planification CPU. Bien que ces coûts soient minimes pour l'utilisation des goroutines, les générer en grande quantité sans cycle de vie contrôlé peut entraîner des problèmes de performance sérieux. Des goroutines avec des cycles de vie non gérés peuvent également entraîner d'autres problèmes, tels que l'empêchement des objets inutilisés d'être collectés par le ramasse-miettes et la rétention des ressources inutilisées.

Par conséquent, ne laissez pas fuiter les goroutines dans votre code. Utilisez go.uber.org/goleak pour tester les fuites de goroutines au sein d'un package susceptible de produire des goroutines.

En général, chaque goroutine doit :

  • Avoir un temps d'arrêt prévisible ; ou
  • Avoir un moyen de signaler à la goroutine qu'elle doit s'arrêter.

Dans les deux cas, il doit y avoir un moyen pour le code de bloquer et d'attendre que la goroutine se termine.

Par exemple :

Non recommandé :

go func() {
  for {
    flush()
    time.Sleep(delay)
  }
}()
// Il n'y a aucun moyen d'arrêter cette goroutine. Elle s'exécutera indéfiniment jusqu'à ce que l'application se ferme.

Recommandé :

var (
  stop = make(chan struct{}) // Signale à la goroutine de s'arrêter
  done = make(chan struct{}) // Indique que la goroutine est sortie
)
go func() {
  defer close(done)
  ticker := time.NewTicker(delay)
  defer ticker.Stop()
  for {
    select {
    case <-ticker.C:
      flush()
    case <-stop:
      return
    }
  }
}()

// ... autre code
close(stop)  // Indique que la goroutine doit s'arrêter
<-done       // et attendez qu'elle se termine

// Cette goroutine peut être attendue avec close(stop), et nous pouvons attendre qu'elle se termine avec <-done.

Attente de la sortie des Goroutines

Lorsqu'une goroutine générée par le système est donnée, il doit y avoir un moyen d'attendre que la goroutine se termine. Il existe deux méthodes courantes pour y parvenir :

  • Utiliser sync.WaitGroup. Utilisez cette méthode lorsque vous attendez plusieurs goroutines.
var wg sync.WaitGroup
for i := 0; i < num; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        // ...
    }()
}
wg.Wait()
  • Ajouter une autre chan struct{}, et la fermer après que la goroutine est terminée. Utilisez cette méthode s'il n'y a qu'une seule goroutine.
done := make(chan struct{})
go func() {
    defer close(done)
    // ...
}()
// Pour attendre que la goroutine se termine :
<-done

La fonction init() ne doit pas créer de goroutines. Voir également Évitez d'utiliser init().

Si un package nécessite une goroutine de fond, il doit exposer un objet responsable de la gestion du cycle de vie de la goroutine. Cet objet doit fournir une méthode (Close, Stop, Shutdown, etc.) pour indiquer la terminaison de la goroutine de fond et attendre sa sortie.

Approche non recommandée :

func init() {
    go doWork()
}
func doWork() {
    for {
        // ...
    }
}
// Lorsque l'utilisateur exporte ce package, une goroutine de fond est générée de manière inconditionnelle. Les utilisateurs ne peuvent pas contrôler la goroutine ou l'arrêter.

Approche recommandée :

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 indique au travailleur de s'arrêter
// et attend qu'il se termine.
func (w *Worker) Shutdown() {
    close(w.stop)
    <-w.done
}

Générez le travailleur uniquement lorsque l'utilisateur en fait la demande et fournissez une méthode pour arrêter le travailleur afin que l'utilisateur puisse libérer les ressources utilisées par le travailleur.

Notez que si le travailleur gère plusieurs goroutines, un WaitGroup doit être utilisé.