Как устроены горутины, сколько памяти они занимают в стеке?go-61

Архитектура горутины

Горутина в Go состоит из нескольких ключевых компонентов:

  1. Стек - сегмент памяти для хранения локальных переменных и вызовов функций
  2. Дескриптор (g структура в runtime) - содержит метаданные и контекст выполнения
  3. Связь с планировщиком - указатели на связанные M (поток ОС) и P (логический процессор)
// Упрощенная структура горутины из runtime/runtime2.go
type g struct {
    stack       stack   // Описание стека (нижняя и верхняя границы)
    stackguard0 uintptr // Проверка переполнения стека
    sched       gobuf   // Контекст выполнения (регистры, PC, SP)
    m           *m      // Привязанный поток ОС
    atomicstatus uint32 // Текущее состояние горутины
}

Модель стека горутин

Начальный размер стека

  • Go 1.2-1.3: 8KB
  • Go 1.4+: 2KB (оптимизация для уменьшения памяти)

Динамическое управление стеком

Горутины используют сегментированные (split) стеки:

  1. При создании выделяется начальный стек (2KB)
  2. При нехватке места:
    • Создается новый стек в 2 раза больше
    • Все данные копируются в новый стек
    • Обновляются указатели (точная копия оригинального стека)
+---------------+
|   Новый стек  | (4KB)
+---------------+
| Копия данных  |
+---------------+
|               |
+---------------+
|   Старый стек | (2KB) -> будет освобожден
+---------------+

Максимальный размер стека

  • 32-битные системы: 250MB
  • 64-битные системы: 1GB

Можно изменить через runtime/debug.SetMaxStack

Потребление памяти

Базовая память на горутину

  1. Дескриптор горутины: ```60-90 байт
  2. Начальный стек: 2KB
  3. Дополнительные аллокации: если горутина использует heap

Пример измерения:

func measureGoroutineMem() {
    var before, after runtime.MemStats

    runtime.ReadMemStats(&before)
    var wg sync.WaitGroup
    wg.Add(10000)

    for i := 0; i < 10000; i++ {
        go func() {
            defer wg.Done()
            runtime.Gosched()
        }()
    }

    wg.Wait()
    runtime.ReadMemStats(&after)

    fmt.Printf("Memory per goroutine: %.2f KB\n",
        float64(after.HeapAlloc-before.HeapAlloc)/10000/1024)
}

Оптимизации стека в рантайме

  1. Сжатие стека:

    • Если стек использует мало памяти, может быть уменьшен
    • Автоматически выполняется сборщиком мусора
  2. Работа с большими стеками:

    • При достижении лимита вызывается runtime.morestack
    • Планировщик может приостановить горутину для выделения памяти

Сравнение с потоками ОС

Параметр Горутина Поток ОС
Начальный размер 2KB 1-8MB
Максимальный размер 1GB Не ограничен
Аллокация Динамическая Фиксированная
Стоимость создания 300ns 1-10µs

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

  1. Избегайте глубокой рекурсии - может вызвать многократное расширение стека
  2. Для CPU-bound задач - ограничьте количество горутин runtime.GOMAXPROCS
  3. Для IO-bound задач - можно создавать тысячи горутин
  4. Мониторинг - используйте runtime.NumGoroutine() для отслеживания количества

Пример безопасного шаблона:

func workerPool(tasks <-chan Task, results chan<- Result, workers int) {
    var wg sync.WaitGroup
    wg.Add(workers)

    for i := 0; i < workers; i++ {
        go func() {
            defer wg.Done()
            for task := range tasks {
                results <- process(task)
            }
        }()
    }

    go func() {
        wg.Wait()
        close(results)
    }()
}

Резюмируем

горутины в Go используют инновационную модель динамических стеков, позволяющую эффективно использовать память. Начальный размер стека всего 2KB делает их исключительно легковесными, а механизм сегментированных стеков обеспечивает гибкость при росте потребностей. Понимание этих механизмов помогает писать высокопроизводительные concurrent-приложения без риска исчерпания памяти.