Zarządzanie Goroutine
Goroutine są lekkie, ale nie są darmowe: w najgorszym przypadku zużywają pamięć na stosie i planowanie procesora. Chociaż koszty te są niewielkie w przypadku użycia Goroutine, generowanie ich w dużych ilościach bez kontrolowanego cyklu życia może prowadzić do poważnych problemów wydajnościowych. Goroutine z niezarządzanymi cyklami życia mogą również prowadzić do innych problemów, takich jak uniemożliwienie odbierania nieużywanych obiektów przez garbage collector oraz zatrzymywanie nieużywanych zasobów.
Dlatego nie wyciekaj goroutin w swoim kodzie. Użyj go.uber.org/goleak do testowania wycieków goroutine wewnątrz pakietu, który może generować goroutiny.
W ogólnym przypadku każda goroutine musi:
- mieć przewidywalny czas zatrzymania; lub
- mieć sposób sygnalizowania goroutiny, że powinna zostać zatrzymana.
W obu przypadkach kod musi być w stanie zablokować się i czekać na zakończenie goroutiny.
Na przykład:
Nie zalecane:
go func() {
for {
flush()
time.Sleep(delay)
}
}()
// Nie ma sposobu na zatrzymanie tej gorutyny. Będzie działać czas nieokreślony, aż do zamknięcia aplikacji.
Zalecane:
var (
stop = make(chan struct{}) // Sygnalizuje zatrzymanie gorutyny
done = make(chan struct{}) // Sygnalizuje zakończenie gorutyny
)
go func() {
defer close(done)
ticker := time.NewTicker(delay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
flush()
case <-stop:
return
}
}
}()
// ... inny kod
close(stop) // Wskazuje, że gorutyna powinna zostać zatrzymana
<-done // i czeka na jej zakończenie
// Na tę goroutynę można poczekać za pomocą close(stop), a następnie oczekiwać zakończenia z <-done.
Oczekiwanie na Zakończenie Goroutine
Kiedy dana jest gorutyna generowana przez system, powinien istnieć sposób oczekiwania na jej zakończenie. Istnieją dwa powszechnie stosowane metody realizacji tego celu:
- Użycie
sync.WaitGroup
. Użyj tej metody, gdy oczekujesz na zakończenie wielu goroutyn.
var wg sync.WaitGroup
for i := 0; i < num; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// ...
}()
}
wg.Wait()
- Dodanie innego
chan struct{}
i zamknięcie go po zakończeniu gorutyny. Użyj tej metody, jeśli istnieje tylko jedna gorutyna.
done := make(chan struct{})
go func() {
defer close(done)
// ...
}()
// Aby poczekać na zakończenie gorutyny:
<-done
Funkcja init()
nie powinna tworzyć gorutyn. Zobacz również Unikaj używania init().
Jeśli pakiet wymaga tła goroutyny, musi udostępniać obiekt odpowiedzialny za zarządzanie cyklem życia gorutyny. Obiekt ten musi dostarczać metodę (Close
, Stop
, Shutdown
, itp.) wskazującą zakończenie tła goroutyny i oczekiwania na jej zakończenie.
Nie zalecane podejście:
func init() {
go doWork()
}
func doWork() {
for {
// ...
}
}
// Gdy użytkownik eksportuje ten pakiet, bieżąca gorutyna jest wygenerowana bezwarunkowo. Użytkownicy nie mogą kontrolować ani zatrzymać gorutyny.
Zalecane podejście:
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 informuje pracownika o zatrzymaniu
// i czeka na zakończenie.
func (w *Worker) Shutdown() {
close(w.stop)
<-w.done
}
Generuj pracownika tylko wtedy, gdy zostanie to zażądane przez użytkownika, i udostępnij metodę wyłączenia pracownika, aby użytkownik mógł zwolnić zasoby wykorzystywane przez pracownika.
Należy zauważyć, że jeśli pracownik zarządza wieloma gorutynami, należy użyć WaitGroup.