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

Как запустить 100 тыс. горутин?

2.0 Middle🔥 141 комментариев
#Конкурентность и горутины#Производительность и оптимизация

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

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

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

Запуск 100 тысяч горутин в Go: практические аспекты

Запустить 100 тысяч горутин технически очень просто благодаря легковесной природе горутин (goroutines) - они представляют собой пользовательские потоки, управляемые рантаймом Go, с минимальным потреблением памяти (около 2-8 КБ в стеке). Однако простота запуска не означает отсутствия подводных камней при работе с таким количеством параллельных задач.

Базовый пример запуска

package main

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

func main() {
    var wg sync.WaitGroup
    totalGoroutines := 100_000
    
    for i := 0; i < totalGoroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // Имитация работы
            time.Sleep(10 * time.Millisecond)
            fmt.Printf("Горутина %d завершена\n", id)
        }(i)
    }
    
    wg.Wait()
    fmt.Println("Все горутины завершены")
}

Ключевые аспекты и рекомендации

1. Управление памятью и стеком

Горутины начинают с очень маленького стека (обычно 2 КБ), который динамически растет при необходимости. Для 100k горутин минимальное потребление составит примерно 200 МБ только под стеки, без учета данных самой программы.

// Плохо: захват переменной цикла
for i := 0; i < 100000; i++ {
    go func() {
        fmt.Println(i) // Все горутины увидят одно значение!
    }()
}

// Правильно: передача параметра
for i := 0; i < 100000; i++ {
    go func(id int) {
        fmt.Println(id) // Каждая горутина получит уникальное значение
    }(i)
}

2. Контроль параллелизма и ограничение ресурсов

Запуск 100k горутин, которые активно работают, может создать проблемы:

// Использование семафора для ограничения одновременных горутин
func runWithLimit(total, limit int) {
    var wg sync.WaitGroup
    semaphore := make(chan struct{}, limit) // Семафор на limit одновременных горутин
    
    for i := 0; i < total; i++ {
        wg.Add(1)
        semaphore <- struct{}{} // Занимаем слот
        
        go func(id int) {
            defer wg.Done()
            defer func() { <-semaphore }() // Освобождаем слот
            
            processTask(id)
        }(i)
    }
    
    wg.Wait()
}

3. Обработка паник и graceful shutdown

При работе с большим количеством горутин критически важна обработка ошибок:

func safeGoroutine(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Горутина %d восстановлена после паники: %v", id, r)
        }
    }()
    
    // Основная логика
    riskyOperation(id)
}

4. Использование worker pool для задач с IO

Для IO-задач эффективнее использовать пул воркеров:

func workerPool(tasks <-chan int, numWorkers int) {
    var wg sync.WaitGroup
    
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            for task := range tasks {
                processIO(task)
            }
        }(i)
    }
    
    // Отправка задач в канал
    // ...
    
    close(tasks)
    wg.Wait()
}

Потенциальные проблемы и их решение

  1. Потребление памяти: Мониторинг через runtime.ReadMemStats
  2. Блокировка планировщика: Избегать длительных вычислений без вызовов, которые могут привести к перепланированию
  3. Утечки горутин: Всегда обеспечивать условия выхода из горутин
  4. Contention на синхронизации: Использовать sync.Pool для объектов, atomic операции где возможно

Мониторинг и диагностика

import "runtime"

func printGoroutineStats() {
    for {
        time.Sleep(5 * time.Second)
        fmt.Printf("Количество горутин: %d\n", runtime.NumGoroutine())
        
        var m runtime.MemStats
        runtime.ReadMemStats(&m)
        fmt.Printf("Память: %v MB\n", m.Alloc/1024/1024)
    }
}

Когда 100k горутин оправданы?

  • Обработка множества медленных IO-операций (сетевые запросы, работа с файлами)
  • Реализация chat-серверов с множеством подключений
  • Системы событийного программирования
  • Задачи, где время жизни горутин короткое, а большую часть времени они ожидают

Выводы

Запуск 100 тысяч горутин технически несложен, но требует:

  • Внимания к памяти и возможному росту стеков
  • Контроля за утечками горутин
  • Правильного шаблона завершения всех горутин
  • Мониторинга количества горутин и потребления памяти

Для CPU-задач чаще эффективнее использовать количество горутин, равное количеству CPU ядер или близкое к нему, с использованием пула воркеров. Для IO-задач большое количество горутин часто оправдано, но требует грамотного управления ресурсами и ограничения одновременной активности.

Как запустить 100 тыс. горутин? | PrepBro