Какие существуют паттерны для graceful shutdown (корректного завершения) горутин?

Ответ

В Go нет механизма принудительного завершения горутины извне. Вместо этого используется паттерн кооперативной отмены, когда горутина сама проверяет сигнал о необходимости завершения и корректно прекращает свою работу.

Существует два основных способа для реализации этого:

1. Использование сигнального канала

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

// worker ожидает сигнала в канале stopCh
func worker(id int, wg *sync.WaitGroup, stopCh <-chan struct{}) {
    defer wg.Done()
    fmt.Printf("Воркер %d запущенn", id)
    for {
        select {
        case <-stopCh:
            // Получен сигнал на завершение
            fmt.Printf("Воркер %d останавливаетсяn", id)
            return
        default:
            // Выполнение полезной работы
            fmt.Printf("Воркер %d работает...n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    var wg sync.WaitGroup
    stopCh := make(chan struct{})

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, &wg, stopCh)
    }

    // Даем воркерам поработать 3 секунды
    time.Sleep(3 * time.Second)

    // Отправляем сигнал на завершение всем воркерам
    fmt.Println("Отправка сигнала на остановку...")
    close(stopCh)

    // Ожидаем, пока все воркеры корректно завершат работу
    wg.Wait()
    fmt.Println("Все воркеры остановлены.")
}
  • close(stopCh): Закрытие канала немедленно разблокирует все ожидающие операции чтения (<-stopCh), которые получают нулевое значение. Это делает его идеальным для одновременной сигнализации множеству горутин.
  • sync.WaitGroup: Используется для того, чтобы main горутина дождалась фактического завершения всех дочерних горутин перед выходом из программы.

2. Использование context.Context

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

func workerWithContext(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            // Контекст был отменен, завершаем работу
            fmt.Println("Завершение работы через контекст...")
            return
        default:
            // Полезная работа
            fmt.Println("Работаю...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    var wg sync.WaitGroup

    wg.Add(1)
    go workerWithContext(ctx, &wg)

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

    wg.Wait() // Ожидаем завершения горутины
    fmt.Println("Горутина завершена.")
}
  • context.WithCancel: Создает контекст, который можно отменить вручную вызовом функции cancel().
  • <-ctx.Done(): Канал Done() закрывается, когда вызывается cancel(), что служит сигналом для завершения.