⚙️ Go Runtime изнутри: GMP, GC, escape analysis и memory model — 11 июля 2026 г. в 05:52:01.012
⚙️ Go Runtime изнутри: GMP, GC, escape analysis и memory model Почему Go "просто работает" быстро без ручного управления потоками — разбираем механику под капотом. 1. GMP-модель планировщика Три сущности: - G (Goroutine) — сама горутина: стек (растёт от 2KB), инструкция, статус - M (Machine) — реальный OS-поток, который выполняет код - P (Processor) — логический процессор, держит локальную очередь горутин (runqueue) и служит "разрешением" на выполнение Ключевая идея: GOMAXPROCS задаёт число P, а не M. M может блокироваться на syscall — тогда P отвязывается от него и находит себе другой свободный/новый M. Это и есть секрет: блокирующий syscall не останавливает остальные горутины. Work stealing: если у P опустела локальная очередь, он крадёт горутины у других P (обычно половину батча) или лезет в глобальную очередь. Это балансировка без централизованного шедулера. Preemption: до Go 1.14 планировщик был кооперативным — горутина без вызовов функций могла зависнуть навечно в tight loop. С 1.14 добавлен асинхронный preemption через сигналы (SIGURG), горутину прерывают принудительно. 2. Escape Analysis Компилятор решает на этапе компиляции — стек или куча: func onStack() int { x := 42 return x // не убегает — на стеке } func onHeap() *int { x := 42 return &x // адрес утекает — уходит в кучу } Проверить реальность: go build -gcflags="-m" main.go Частые причины "утечки" на кучу: возврат указателя, передача в интерфейс, замыкание, которое переживает функцию, слишком большой объект (компилятор консервативен). 3. GC: Concurrent Mark & Sweep Go использует tri-color mark-and-sweep с write barrier, работающий конкурентно с мутатором (вашей программой): - White — потенциальный мусор - Grey — найден, но дети не просканированы - Black — жив, обработан полностью Write barrier ловит запись указателя во время фазы маркировки, чтобы не потерять объект, если мутатор переставляет ссылки прямо во время сборки (проблема "затирания" грей-объекта). STW (Stop-The-World) случается только дважды за цикл, и оба раза — микросекунды: старт (включить write barrier) и финиш (выключить, финализировать). Триггер GC — GOGC (по умолчанию 100%: сборка запускается, когда куча выросла вдвое с прошлого цикла) и с Go 1.19 — GOMEMLIMIT для soft memory limit. 4. Memory Model: happens-before Go memory model формально описывает, при каких условиях запись в одной горутине гарантированно видна чтению в другой. Без синхронизации — никаких гарантий, компилятор и процессор вправе переупорядочить операции. Гарантии happens-before дают: // 1. Channel ch := make(chan int) go func() { data = 42 // (A) ch <- 1 // (B) happens-before получение }() <-ch // (C) _ = data // видит 42, т.к. A → B → C // 2. Mutex mu.Lock() // критическая секция mu.Unlock() // Unlock happens-before следующий Lock // 3. sync.Once var once sync.Once once.Do(f) // f гарантированно выполнится один раз, видимо всем Без синхронизации — это data race, даже если "по факту не ломается" на вашей машине. Проверяйте: go run -race main.go Гонка данных в Go — undefined behavior на уровне спецификации, а не просто "риск получить неверное значение". GMP даёт дешёвую конкурентность, escape analysis решает, где жить переменной, GC работает конкурентно и почти без пауз, а memory model — это контракт, без соблюдения которого все остальные гарантии бессмысленны. #golang #runtime #gc #scheduler #concurrency 👉 @golang_lib

