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

Как происходит выделение памяти при работе потока?

1.0 Junior🔥 123 комментариев
#Операционные системы и Linux#Производительность и оптимизация

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

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

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

Выделение памяти в потоках Go (горутинах)

В Go выделение памяти для потока (горутины) — это многоуровневый процесс, который существенно отличается от традиционных потоков операционной системы. Горутины — это легковесные потоки, управляемые рантаймом Go, а не ОС.

Иерархия памяти горутины

Оперативная память процесса Go
    ↓
Куча (Heap) — общая для всех горутин
    ↓
Стек горутины — индивидуальный для каждой горутины (от 2KB по умолчанию)
    ↓
Регистры процессора и контекст выполнения

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

1. Инициализация стека горутины

При создании горутины через go func() рантайм выделяет начальный стек размером 2KB (значение может меняться в разных версиях Go):

// Пример создания горутины
func main() {
    go func() {
        // У этой горутины свой стек 2KB
        x := 42 // Локальная переменная в стеке
        fmt.Println(x)
    }()
    
    time.Sleep(100 * time.Millisecond)
}

2. Динамическое расширение стека

Стек горутины не фиксированного размера и может динамически расти и сокращаться:

  • При нехватке места (stack overflow) стек копируется в новый участок памяти в 2 раза больше
  • Копирование происходит прозрачно, указатели на объекты в старом стеке обновляются
  • Максимальный размер стека: 1GB на 64-битных системах, 250MB на 32-битных
func recursiveDeep(depth int) {
    var buffer [256]byte // Выделяется в стеке
    if depth > 0 {
        recursiveDeep(depth - 1)
    }
}
// При глубокой рекурсии стек будет расширяться

3. Разделение кучи (Heap)

Все горутины одного процесса разделяют общую кучу:

  • Объекты, созданные с помощью new() или make(), обычно попадают в кучу
  • Переменные, адрес которых "убегает" (escape) из функции, также размещаются в куче
func createPointer() *int {
    x := 42 // x "убегает" из функции → выделяется в куче
    return &x
}

func main() {
    p := createPointer() // Указатель на объект в куче
    go func() {
        fmt.Println(*p) // Все горутины имеют доступ к куче
    }()
}

4. Локальные пулы (P) и кэши потоков

Рантайм Go использует сложную систему управления памятью с локальными пулами:

Глобальная куча (mheap)
    ↓
Центральные кэши (mcentral) для разных размеров
    ↓
Локальные кэши (mcache) для каждого процессорного ядра (P)
    ↓
Выделение памяти для конкретной горутины

Каждый логический процессор (P) имеет свой локальный кэш памяти, что минимизирует блокировки при выделении памяти из кучи.

Процесс выделения памяти в деталях

  1. Локальные переменные размещаются в стеке горутины
  2. Динамические данные размещаются в куче с использованием сборщика мусора
  3. Сегменты стека хранятся в куче, но доступны только "своей" горутине
  4. Выделение в куче происходит через аллокатор Go, который:
    • Использует размерные классы (8, 16, 32, 48, ..., 32768 байт)
    • Применяет технику bump allocation в текущем span
    • При нехватке памяти запрашивает новые страницы у ОС
// Разные стратегии выделения
func memoryAllocationExamples() {
    // В стеке (если не убегает)
    localInt := 42
    
    // В куче (убегает)
    escaped := map[string]int{"key": 123}
    
    // В куче через make
    slice := make([]byte, 1024) // 1KB в куче
    
    // Массив в стеке
    var array [64]byte // 64 байта в стеке
}

Оптимизации и особенности

  • Стеки начинаются маленькими (2KB), что позволяет создавать миллионы горутин
  • Копирование стека дешевле, чем страничные ошибки (page faults) в системных потоках
  • Разделение кучи требует синхронизации, но локальные кэши минимизируют конфликты
  • Сборщик мусора работает с кучей, но не затрагивает стеки напрямую
  • Адресное пространство стека может быть разрывным (не одним непрерывным регионом)

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

АспектГорутина GoСистемный поток (pthread)
Размер стека2KB начально, динамический1-8MB фиксированный (обычно)
Выделение стекаРантаймом Go из кучиОС через syscall (mmap/VirtualAlloc)
Переключение~100-200ns (в пространстве пользователя)~1000-1500ns (требуется ядро ОС)
Память кучиОбщая, со сборкой мусораОбщая, но без GC (ручное управление)

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

  1. Избегайте утечек указателей из стека в кучу без необходимости
  2. Используйте sync.Pool для часто создаваемых временных объектов
  3. Контролируйте глубину рекурсии — хотя стек расширяется, копирование дорого
  4. Профилируйте выделение памяти с помощью pprof:
    go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap
    

Выделение памяти для горутин в Go — это баланс между производительностью, безопасностью и эффективным использованием ресурсов. Динамические стеки и общая куча со сборкой мусора позволяют создавать высококонкурентные приложения с минимальными накладными расходами на управление памятью.

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

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

Выделение памяти в потоке в Go

В Go выделение памяти для потока (горутины) — это комплексный процесс, тесно связанный с работой планировщика (scheduler) и системой управления памятью (memory management). Давайте разберем его поэтапно.

Инициализация стека горутины

Каждая горутина начинается с небольшого предвыделенного стека (initial stack), размер которого зависит от архитектуры:

  • Обычно 2 КБ на 64-битных системах.
  • Может быть 4 КБ на 32-битных системах или других архитектурах.

Этот начальный стек выделяется из пула стека (stack pool) планировщика для минимизации накладных расходов.

// Пример горутины. При её запуске планировщик инициализирует стек.
go func() {
    // Локальные переменные этой функции размещаются на стеке горутины
    localVar := 42
    fmt.Println(localVar)
}()

Динамический рост стека (Stack Growing)

В отличие от фиксированных стеков в системных потоках, Go использует сегментированные стеки (segmented stacks), а начиная с Go 1.4 — преимущественно технику непрерывных стеков (contiguous stacks или stack copying).

Процесс роста:

  1. Когда горутине не хватает места на стеке (например, при глубокой рекурсии), возникает "ловушка переполнения стека" (stack overflow trap).
  2. Планировщик приостанавливает выполнение горутины.
  3. Выделяется новый стек в 2 раза больше предыдущего (обычно, пока не достигнет максимума).
  4. Происходит копирование всего содержимого старого стека в новый.
  5. Указатели на стек корректируются (это нетривиальная задача, решаемая сборщиком мусора и системой времени выполнения).
  6. Выполнение горутины возобновляется с новым стеком.
func recursiveFunction(depth int) {
    var buffer [256]byte // Локальный массив размещается на стеке
    if depth == 0 {
        return
    }
    recursiveFunction(depth - 1) // Глубокий вызов может вызвать рост стека
}

Выделение памяти в куче (Heap Allocation)

Не вся память горутины живет на стеке. Память в куче (heap) выделяется в случаях:

  • Когда на переменную существует ссылку после возврата из функции (escape analysis).
  • При использовании new, make (для сложных типов) или композитных литералов, если компилятор решает, что они "сбегают" (escape).
  • Для глобальных переменных.
  • При явном выделении через malloc в рантайме.

Пример escape-анализа:

func createInt() *int {
    v := 42 // Переменная v "сбегает" из функции, поэтому будет выделена в куче.
    return &v
}

func useSlice() {
    s := make([]int, 1000) // Большой срез, вероятно, будет выделен в куче.
    // ... использование s
}

Управление планировщиком и системные потоки

  • Планировщик Go (M:N scheduler) назначает множество горутин на ограниченное количество системных потоков (OS threads, или M).
  • Каждому системному потоку выделяется собственный, большой, фиксированный стек (обычно 1-8 МБ, зависит от ОС). Это делается операционной системой.
  • Память для структур данных планировщика (очереди готовых горутин, свободный список M и т.д.) выделяется в куче при старте программы.

Оптимизации и пулы

  • Синхронизация без блокировок (lock-free structures) используется в планировщике для минимизации конкуренции.
  • Пулы потоков (thread pools) и пулы стеков (stack pools) используются для повторного использования ресурсов и снижения накладных расходов.

Примерная схема процесса

  1. Запуск горутины: Планировщик берет готовый стек из пула (или выделяет новый небольшой).
  2. Выполнение: Локальные переменные и вызовы размещаются на этом стеке.
  3. Нехватка стека: Триггер для роста → выделение нового большего блока памяти и копирование.
  4. "Побег" данных: Компилятор определяет, что данные должны жить дольше функции → выделение в куче через runtime.mallocgc.
  5. Системный вызов или блокировка: Если горутина блокируется (например, на I/O), планировщик может отвязать её от текущего системного потока и привязать другую готовую горутину, не трогая их стеки.
  6. Завершение: Стек горутины возвращается в пул для повторного использования.

Сборка мусора (Garbage Collection)

Выделенная в куче память управляется сборщиком мусора (GC). Горутины работают как корни для триколорного алгоритма (используются для обхода живых объектов). Важно: стек горутины сканируется GC как активная память.

Таким образом, выделение памяти для потока в Go — это гибридная система: быстрые предвыделенные сегменты стека с динамическим ростом + выделение в куче при необходимости, управляемое компилятором (escape analysis) и сборщиком мусора, что обеспечивает баланс между производительностью и гибкостью, кардинально отличаясь от классической модели потоков ОС.