1. Внутреннее устройство Go: планировщик, управление памятью и сборщик мусора
Внутреннее устройство Go: планировщик, управление памятью и сборщик мусора
Добро пожаловать в курс Go Middle. Чтобы перейти от уровня Junior к Middle и выше, недостаточно просто уметь писать код, который работает. Необходимо понимать, как он работает «под капотом». Это знание позволяет писать высокопроизводительные приложения, эффективно отлаживать сложные проблемы и, конечно же, успешно проходить технические собеседования, где вопросы о внутреннем устройстве Go (Runtime) являются стандартом.
В этой статье мы разберем три кита, на которых держится Go Runtime: планировщик (Scheduler), аллокатор памяти и сборщик мусора (Garbage Collector).
Планировщик Go (Go Scheduler)
Одна из главных фишек Go — это легковесные потоки, называемые горутинами (goroutines). Но как Go удается запускать десятки тысяч горутин на машине с всего лишь несколькими ядрами процессора, не убивая производительность переключениями контекста?
Ответ кроется в модели M:N, где M горутин мультиплексируются на N потоков операционной системы (OS Threads). Этим занимается планировщик Go.
Модель GMP
Для понимания работы планировщика нужно знать три ключевые сущности, образующие аббревиатуру GMP:
GOMAXPROCS).!Схематичное изображение взаимодействия Процессора, Потока ОС и Горутин в модели GMP
Как это работает вместе?
Каждый P имеет свою локальную очередь выполнения (Local Run Queue). Когда вы запускаете go func(), новая G попадает в локальную очередь того P, на котором сейчас выполняется код.
M забирает G из локальной очереди P и выполняет её. Когда G завершается или блокируется, M берет следующую.
Work Stealing (Кража работы)
Что произойдет, если у одного P закончились горутины, а у другого их много? Простаивающий P не будет ждать. Он попытается «украсть» половину горутин из локальной очереди другого P. Если там пусто, он проверит Глобальную очередь (Global Run Queue).
Этот алгоритм называется Work Stealing и позволяет эффективно балансировать нагрузку между ядрами.
Обработка системных вызовов (Handoff)
Если горутина делает блокирующий системный вызов (например, чтение файла), поток M тоже блокируется. Чтобы не терять вычислительную мощность P, происходит процедура Handoff (передача):
Управление памятью (Memory Management)
Управление памятью в Go вдохновлено аллокатором TCMalloc (Thread-Caching Malloc). Главная цель — минимизировать блокировки при выделении памяти в многопоточной среде.
Стек и Куча (Stack vs Heap)
Память делится на две основные области:
* Стек (Stack): Быстрая память, привязанная к конкретной горутине. Выделение и очистка происходят мгновенно (просто сдвигом указателя). Стек горутин динамический: он начинается с 2 КБ и может расти при необходимости. * Куча (Heap): Общая память для всех горутин. Здесь хранятся объекты, которые должны жить дольше, чем функция, их создавшая. Куча требует участия сборщика мусора.
Escape Analysis (Анализ побега)
Как Go решает, куда положить переменную — в стек или в кучу? Этим занимается компилятор на этапе Escape Analysis.
Если компилятор видит, что ссылка на переменную «убегает» из функции (например, возвращается из функции или передается в другую горутину), он помещает её в кучу. Если переменная используется только внутри функции — она остается на стеке.
> Знание Escape Analysis позволяет оптимизировать программы, снижая нагрузку на GC. Проверить решения компилятора можно командой go build -gcflags="-m".
Структура аллокатора
Чтобы выделять память быстро, Go использует иерархическую структуру:
!Иерархия аллокатора памяти: от локального кэша процессора до глобальной кучи
Сборщик мусора (Garbage Collector)
Go использует Concurrent Mark and Sweep (конкурентный, маркирующий и очищающий) сборщик мусора. Его главная цель — низкая задержка (low latency), а не максимальная пропускная способность.
Алгоритм Tricolor Mark and Sweep
GC использует концепцию трех цветов для маркировки объектов:
* Белый: Потенциальный мусор. Объекты, которые еще не были просмотрены сборщиком. * Серый: Объекты, которые помечены как «живые», но их дочерние ссылки еще не просканированы. * Черный: Гарантированно «живые» объекты, чьи ссылки уже полностью просканированы.
Процесс выглядит так:
!Визуализация трехцветного алгоритма маркировки объектов
Write Barrier (Барьер записи)
Поскольку GC работает параллельно с выполнением вашей программы, может возникнуть ситуация, когда программа (мутатор) меняет ссылки во время сканирования. Например, черный объект начинает ссылаться на белый. Если ничего не сделать, белый объект будет удален, хотя он нужен.
Для предотвращения этого используется Write Barrier. Это небольшой кусочек кода, который выполняется при каждой записи указателя в память. Он гарантирует, что инварианты трехцветного алгоритма не нарушаются (например, принудительно красит новый объект в серый цвет).
Stop The World (STW)
Хотя GC в Go называется конкурентным, он не полностью избавлен от пауз Stop The World, когда выполнение программы полностью останавливается. Однако эти паузы очень коротки:
В современных версиях Go паузы STW обычно составляют доли миллисекунды.
Когда запускается GC?
Запуск контролируется переменной окружения GOGC (по умолчанию 100). Формула расчета целевого размера кучи для следующего запуска выглядит так:
где — целевой размер кучи (Target Heap), при достижении которого запустится GC, — размер живой кучи (Live Heap) после предыдущей сборки, а — значение GOGC.
Например, если после сборки у нас осталось 10 МБ живых данных, а GOGC=100, то следующая сборка начнется, когда куча вырастет до:
где — текущий объем данных, — процент прироста.
Заключение
Понимание GMP, аллокации памяти и работы GC — это фундамент для написания эффективного кода на Go. Вы теперь знаете, что горутины — это не магия, а умное планирование поверх потоков ОС, и что память не бесконечна, но эффективно управляется сложной иерархией кэшей.
В следующих статьях мы углубимся в примитивы синхронизации, чтобы научиться безопасно работать с общими данными в этой конкурентной среде.