Какие существуют способы для синхронизации и завершения группы горутин?

Ответ

Существует два основных подхода: дождаться естественного завершения работы или принудительно сигнализировать об отмене.

1. Ожидание завершения с помощью sync.WaitGroup

Это идиоматичный способ дождаться, пока группа горутин завершит свою работу. WaitGroup работает как потокобезопасный счетчик.

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    // Уменьшаем счетчик на 1, когда горутина завершается.
    // defer гарантирует выполнение, даже если в функции произойдет паника.
    defer wg.Done()

    fmt.Printf("Рабочий %d началn", id)
    time.Sleep(time.Second) // Симуляция работы
    fmt.Printf("Рабочий %d закончилn", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        // Увеличиваем счетчик перед запуском каждой горутины
        wg.Add(1)
        go worker(i, &wg)
    }

    // Блокируем выполнение main, пока счетчик WaitGroup не станет равен 0
    wg.Wait()

    fmt.Println("Все рабочие завершили свою работу.")
}

Ключевые методы:

  • wg.Add(n): Увеличивает счетчик на n.
  • wg.Done(): Уменьшает счетчик на 1.
  • wg.Wait(): Блокирует выполнение до тех пор, пока счетчик не станет равен нулю.

2. Сигнал к отмене с помощью context

Если нужно не просто дождаться, а иметь возможность принудительно остановить все горутины (например, по тайм-ауту или из-за ошибки), используется пакет context.

// Концептуальный пример
ctx, cancel := context.WithCancel(context.Background())

// Запускаем горутины, передавая им ctx
for i := 0; i < 10; i++ {
    go func(ctx context.Context) {
        for {
            select {
            case <-ctx.Done(): // Если пришел сигнал отмены
                fmt.Println("Горутина получила сигнал завершения")
                return // Завершаем работу
            default:
                // ... выполняем полезную работу ...
            }
        }
    }(ctx)
}

// Через какое-то время решаем остановить все горутины
time.Sleep(2 * time.Second)
cancel() // Эта функция отправит сигнал в <-ctx.Done() всем горутинам

Этот подход является предпочтительным для управления жизненным циклом горутин в современных Go-приложениях.