← Назад к вопросам

Как завершить толпу горутин?

1.0 Junior🔥 221 комментариев
#Конкурентность и горутины

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как завершить толпу горутин (graceful shutdown)

Завершение множества горутин — критически важная задача для создания надежных Go-Aприложений. Неправильное завершение может привести к утечкам памяти, потере данных или дедлокам. Я рассмотрю основные паттерны, от простых к сложным.

Базовый подход: использование канала done или context

Самый распространенный способ — использование канала для сигнала остановки или контекста (context.Context).

Пример с каналом done:

func worker(id int, done <-chan struct{}) {
    for {
        select {
        case <-done:
            fmt.Printf("Worker %d stopping\n", id)
            return
        default:
            // Выполняем полезную работу
            time.Sleep(500 * time.Millisecond)
            fmt.Printf("Worker %d working\n", id)
        }
    }
}

func main() {
    const numWorkers =一个重要技能
    done := make(chan struct{})
    
    // Запускаем горутины
    for i := 0; i < numWorkers; i++ {
        go worker(i, done)
    }
    
    // Даем горутинам поработать
    time.Sleep(2 * time.Second)
    
    // Сигнал остановки всем горутинам
    close(done)
    
    // Даем время на завершение
    time.Sleep(1 * time.Second)
}

Пример с context.Context (более современный подход):

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
            return
        default:
            // Полезная работа
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // Гарантируем отмену при выходе
    
    for i := 0; i < 5; i++ {
        go worker(ctx, i)
    }
    
    time.Sleep(2 * time.Second)
    cancel() // Сигнал остановки
    time.Sleep(500 * time.Millisecond)
}

Продвинутые стратегии для "толпы" горутин

1. Использование sync.WaitGroup для ожидания завершения

Когда нужно дождаться завершения всех горутин:

func worker(wg *sync.WaitGroup, done <-chan struct{}, id int) {
    defer wg.Done() // Уменьшаем счетчик при завершении
    
    for {
        select {
        case <-done:
            fmt.Printf("Worker %d finishing\n", id)
            return
        default:
            time.Sleep(time.Duration(id) * 100 * time.Millisecond)
        }
    }
}

func main() {
    var wg sync.WaitGroup
    done := make(chan struct{})
    
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go worker(&wg, done, i)
    }
    
    time.Sleep(1 * time.Second)
    close(done)
    wg.Wait() // Блокируемся, пока все горутины не завершатся
    fmt.Println("All workers stopped")
}

2. Комбинирование context и WaitGroup

Идеальный подход для production-кода:

func processBatch(ctx context.Context, wg *sync.WaitGroup, batchID int) {
    defer wg.Done()
    
    for i := 0; i < 3; i++ {
        select {
        case <-ctx.Done():
            fmt.Printf("Batch %d canceled\n", batchID)
            return
        case <-time.After(300 * time.Millisecond):
            // Имитация работы
            fmt.Printf("Batch %d, step %d\n", batchID, i)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    var wg sync.WaitGroup
    
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go processBatch(ctx, &wg, i)
    }
    
    wg.Wait() // Ожидаем либо завершения, либо отмены контекста
}

3. Использование errgroup для обработки ошибок

Пакет golang.org/x/sync/errgroup предоставляет элегантный способ управления группой горутин:

import "golang.org/x/sync/errgroup"

func main() {
    g, ctx := errgroup.WithContext(context.Background())
    
    for i := 0; i < 5; i++ {
        id := i
        g.Go(func() error {
            return workerWithError(ctx, id)
        })
    }
    
    // Ждем завершения всех горутин
    if err := g.Wait(); err != nil {
        fmt.Printf("Error occurred: %v\n", err)
    }
}

Критические рекомендации

Что делать с горутинами, которые не отвечают на сигналы?

  1. Таймауты через context.WithTimeout — устанавливайте максимальное время выполнения
  2. Отдельные каналы для принудительного завершения — в крайних случаях
  3. Логирование и мониторинг — отслеживайте "зависшие" горутины

Паттерн graceful shutdown для серверов:

func startServer(stop <-chan struct{}) {
    server := &http.Server{Addr: ":8080"}
    
    go func() {
        <-stop
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()
        server.Shutdown(ctx) // Graceful shutdown
    }()
    
    server.ListenAndServe()
}

Распространенные ошибки:

  1. Не использование defer для cleanup-операций
  2. Забыть вызвать WaitGroup.Done() — приводит к deadlock
  3. Попытка закрыть канал дважды — вызывает panic
  4. Игнорирование ошибок от context.Canceled

Заключение

Для завершения толпы горутин используйте комбинацию:

  • context.Context для распространения сигналов отмены
  • sync.WaitGroup для ожидания завершения
  • Таймауты для предотвращения бесконечного ожидания
  • Каналы для более сложных сценариев синхронизации

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