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

Сколько весит горутина?

2.0 Middle🔥 151 комментариев
#Конкурентность и горутины

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

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

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

Размер стека горутины

Говоря о «весе» горутины в Go, обычно подразумевают объем памяти, который она занимает. Ответ не является фиксированным значением, так как он зависит от нескольких факторов: размера стека, выделяемой кучи под данные, а также версии Go и целевой архитектуры.

Базовый размер стека (ядро горутины)

Сама структура данных горутины (g в рантайме Go) имеет фиксированный размер (несколько сотен байт). Однако ключевой компонент — стек горутины, начальный размер которого:

  • Go 1.2 — 1.3: 8 КБ (8192 байта) на 64-битных системах.
  • Go 1.4: уменьшен до 2 КБ (2048 байт), что позволило создавать миллионы горутин.
  • Go 1.2 — 1.3: 4 КБ на 32-битных системах.
  • Go 1.4+: 2 КБ на 32-битных системах.

Таким образом, минимальный «вес» только что созданной, пустой горутины в современных версиях Go составляет около 2 КБ.

package main

import (
    "runtime"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    n := 100000 // 100 тысяч горутин
    wg.Add(n)
    for i := 0; i < n; i++ {
        go func() {
            defer wg.Done()
            // Пустая горутина
        }()
    }
    wg.Wait()
    // Потребление памяти будет ~200 МБ + накладные расходы
}

Динамический рост стека

Стек горутины не является фиксированным. Он начинается с 2 КБ и динамически растет (и может сжиматься) по мере необходимости. Это важное отличие от потоков ОС (где стек обычно фиксирован и велик — 1-8 МБ).

  • Когда горутине не хватает стековой памяти (например, при глубокой рекурсии или хранении больших значений на стеке), рантайм Go выделяет новый стек вдвое большего размера, копирует в него данные и освобождает старый.
  • Процесс копирования обеспечивает непрерывность адресного пространства стека и высокую эффективность.
func recursive(depth int) {
    var buf [256]byte // Выделяем массив на стеке
    buf[0] = byte(depth)
    if depth == 0 {
        return
    }
    recursive(depth - 1) // Каждый вызов использует стек
}

func main() {
    recursive(1000) // Стек будет расти несколько раз
}

Память в куче (heap)

Настоящий «вес» горутины определяется не только стеком, но и данными, которые она размещает в куче:

  • Создание больших объектов через make() или new().
  • Захват переменных в замыканиях, если они живут дольше функции.
  • Явное или неявное ускользание (escape) переменных в кучу (определяется escape-анализом компилятора).
func heavyGoroutine() {
    // Этот слайс может быть размещен в куче (зависит от анализа),
    // но если его размер неизвестен на этапе компиляции или он возвращается —
    // он уйдёт в кучу.
    largeSlice := make([]byte, 10*1024*1024) // 10 МБ в куче
    _ = largeSlice
}

Практические выводы и измерения

  1. Эффективность по памяти: Горутины исключительно легковесны по сравнению с потоками ОС (2 КБ vs 1-8 МБ). Это позволяет создавать сотни тысяч и даже миллионы горутин в одном процессе.
  2. Измерение: Точный вес можно измерить с помощью профилировщика памяти (pprof) или чтения runtime.MemStats.
  3. Ограничения: Основное ограничение — не память под стеки, а логика приложения и данные в куче. 1 млн горутин, хранящих по 1 КБ каждая, займут уже ~1 ГБ кучи.
  4. Накладные расходы: Планировщик Go также потребляет память на управление горутинами, но эти расходы незначительны на фоне данных приложения.
import (
    "fmt"
    "runtime"
    "runtime/debug"
)

func printMemStats() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Printf("Alloc = %v MiB", m.Alloc/1024/1024)
    fmt.Printf("\tTotalAlloc = %v MiB", m.TotalAlloc/1024/1024)
    fmt.Printf("\tNumGoroutine = %d\n", runtime.NumGoroutine())
}

func main() {
    printMemStats()
    // ... создание горутин ...
    printMemStats()
    runtime.GC()
    debug.FreeOSMemory() // Вернуть память ОС (по возможности)
    printMemStats()
}

Итог

Базовая стоимость горутины — ~2 КБ (стек) + ~0.5 КБ (служебные структуры). Её реальный «вес» в производственной среде определяется данными, которые она обрабатывает и хранит в куче. Динамический стек делает модель памяти гибкой и эффективной, что является одной из ключевых причин популярности Go для высоконагруженных concurrent-приложений.