Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Размер стека горутины
Говоря о «весе» горутины в 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
}
Практические выводы и измерения
- Эффективность по памяти: Горутины исключительно легковесны по сравнению с потоками ОС (2 КБ vs 1-8 МБ). Это позволяет создавать сотни тысяч и даже миллионы горутин в одном процессе.
- Измерение: Точный вес можно измерить с помощью профилировщика памяти (
pprof) или чтенияruntime.MemStats. - Ограничения: Основное ограничение — не память под стеки, а логика приложения и данные в куче. 1 млн горутин, хранящих по 1 КБ каждая, займут уже ~1 ГБ кучи.
- Накладные расходы: Планировщик 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-приложений.