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

Почему потоки имеют изначально стек размером 2 МБ?

2.0 Middle🔥 142 комментариев
#Другое

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

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

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

Вопрос о размере стека потоков в Go

Отличный вопрос! Он затрагивает фундаментальные аспекты дизайны языка Go и его системы исполнения. Важно сразу уточнить: утверждение о том, что потоки Go всегда имеют стек размером 2 МБ, не совсем верно. Это распространённое упрощение. Давайте разберём реальную картину.

Динамические (сегментированные) стеки в Go

В отличие от многих других языков (C, C++, Java), где каждый поток получает фиксированный и непрерывный регион памяти под стек (часто 1-8 МБ, устанавливаемый ОС или рантаймом), Go использует инновационный подход — динамические ("растущие") стеки.

Ключевые принципы:

  • Изначально маленький стек: Новой горутине (не системному потоку!) выделяется очень небольшой стек — 2 КБ, а не 2 МБ. Это сделано для эффективности: создание сотен тысяч горутин становится дешёвым по памяти.
  • Стек растёт по мере необходимости: Если горутине нужно больше места для вызовов функций или хранения локальных переменных, стек автоматически расширяется. Go runtime выделяет новый, больший сегмент памяти, копирует туда содержимое старого стека и продолжает работу.
  • Стек может уменьшаться: Во время сборки мусора, если Go runtime видит, что большая часть стека не используется, он может скопировать его содержимое в новый, меньший сегмент, освобождая память. Этот процесс называется "уменьшение стека" (stack shrinking).

Откуда берётся число 2 МБ?

Число 2 МБ — это совсем другой параметр. Это начальный размер стека для системных потоков ОС, на которых планировщик Go выполняет ваши горутины.

  • Каждый поток, созданный рантаймом Go (например, для системных вызовов, работы сборщика мусора или параллельного выполнения горутин на разных ядрах CPU), действительно получает от операционной системы стандартный размер стека.
  • Этот размер зависит не от Go, а от операционной системы и её настроек.
    *   На Linux (x86-64) по умолчанию это обычно **2-8 МБ**.
    *   На Windows — **1-2 МБ**.
  • Go runtime при создании нового потока просто запрашивает у ОS поток с такими дефолтными характеристиками.
// Пример, иллюстрирующий разницу. Этот код создаёт 100_000 горутин.
// Если бы у каждой был стек 2 МБ, это потребовало бы ~200 ГБ ОЗУ!
// В реальности это потребует несколько сотен мегабайт.

package main

import (
    "runtime"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 100_000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // Некоторая работа с небольшой глубиной вызовов
            time.Sleep(10 * time.Millisecond)
        }(i)
    }
    wg.Wait()
}

Почему такой дизайн? Преимущества и недостатки

Преимущества модели Go:

  • Эффективное использование памяти: Можем иметь миллионы горутин, большинство из которых используют лишь килобайты памяти.
  • Избегание переполнения стека (Stack Overflow): Горутина "просто" расширит стек, если та закончится (хотя есть предел, заданный runtime/debug.SetMaxStack).
  • Высокая скорость создания: Выделение 2 КБ и настройка структур данных планировщика — очень быстрая операция.

Недостатки / Компромиссы:

  • Накладные расходы на копирование: Расширение/уменьшение стека требует выделения памяти и копирования всего содержимого стека. Это может быть дорого для горутин с очень глубокой или "горячей" рекурсией.
  • Сложность реализации: Планировщик и сборщик мусора должны корректно обрабатывать указатели внутри стеков во время их перемещения. Это одна из причин, почему Go до версии 1.3 имел сегментированные, но не перемещаемые стеки.

Сравнение с традиционными потоками

АспектПоток ОС (C++/Java)Горутина Go
Начальный размер стекаБольшой (1-8 МБ, задаётся ОС)Очень маленький (2 КБ)
Изменение размераФиксированный (переполнение = крах)Динамическое (расширяется/сжимается)
Потребление памятиЛинейно растёт с числом потоков (> ~1000 потоков проблематично)Сублинейно, очень эффективно
Скорость созданияОтносительно медленная (работа с ядром ОС)Очень быстрая (пользовательский планировщик)

Практический вывод и управление размером

Хотя размером стека горутин обычно не нужно управлять, в исключительных случаях это можно сделать:

  1. Лимит на рост стека: Пакет runtime/debug позволяет задать максимальный размер.
    import "runtime/debug"
    debug.SetMaxStack(64 * 1024 * 1024) // Ограничиваем стек 64 МБ
    
  2. Настройка системных потоков: Размер стека потоков ОС, которые рантайм создаёт под капотом, настраивается через переменную окружения GODEBUG:
    GODEBUG=stacksize=1048576 ./myapp # Пытаемся задать 1 МБ для системных потоков
    
    Однако эта настройка больше рекомендация для ОС, и она может быть проигнорирована.

Итог: Go сознательно отошёл от модели "один поток — большой фиксированный стек" в пользу модели "много легковесных сущностей с маленькими растущими стеками". Это архитектурное решение — одна из ключевых причин, почему Go так эффективен в создании высоконагруженных concurrent-приложений. Число 2 МБ относится к размеру стека системных потоков рантайма, а не к горутинам, стек которых начинается с 2 КБ.