> Как устроена многозадачность в Go и какой механизм планирования используется (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: ЦУМ

Стек: Go

> Пример ответа

Короткий ответ

В Go многозадачность реализована на основе горутин - легковесных потоков, которые планируются рантаймом, а не операционной системой. Используется механизм M:N-планирования: N горутин выполняются на M OS-потоках. Планировщик Go - это cooperative preemptive scheduler с work-stealing алгоритмом: горутины кооперативно уступают управление на точках прерывания (например, при системных вызовах, каналах, time.Sleep), но также могут быть вытеснены принудительно на границах безопасных инструкций. Основные компоненты: G (горутина), M (машина/поток), P (processor/локальный планировщик).

Подробное объяснение

Многозадачность в Go строится вокруг трёх сущностей:

  • G (goroutine) - лёгкий стек (начинается с 2-8 КБ, растёт динамически), состояние и контекст выполнения.
  • M (machine) - OS-поток, на котором выполняется код. M может блокироваться на системных вызовах, при этом Go может поднять новый M.
  • P (processor) - логический процессор, привязанный к GOMAXPROCS. P владеет локальной очередью готовых горутин (run queue) и определяет, сколько горутин может выполняться параллельно.

Планировщик работает по схеме M:N: горутин может быть десятки тысяч, а потоков - столько, сколько ядер (GOMAXPROCS, по умолчанию равно числу CPU). Каждый P имеет локальную очередь (capacity 256), плюс есть глобальная очередь для переполнения.

Алгоритм планирования:

  1. Когда горутина блокируется (например, на канале, мьютексе, системном вызове), она снимается с P, а P подхватывает следующую из локальной очереди.
  2. Если локальная очередь пуста, P пытается украсть (work stealing) горутины из глобальной очереди или у других P (сначала половину, потом по одной).
  3. При системном вызове, который может заблокировать поток, M отделяется от P, P переходит к другому M, а заблокированный M ждёт завершения syscall и затем возвращается в пул.
  4. Вытеснение: с Go 1.14 планировщик является preemptive - специальный сигнал (SIGURG) прерывает выполнение горутины на безопасных точках (например, при вызове функций или проверке флага preemption в прологе функции). Это предотвращает бесконечные циклы без вызова планировщика.

Ключевые точки переключения контекста:

  • операции с каналами (send/receive)
  • sync-примитивы (Mutex, WaitGroup)
  • time.Sleep, time.After
  • runtime.Gosched() - явная уступка
  • системные вызовы
  • вызовы планировщика при аллокации памяти или GC

Важно: горутины не имеют собственного стека в памяти потока - стек выделяется в куче и растёт/сжимается динамически, поэтому создание горутины стоит ~2-3 КБ и очень дёшево.

На практике

Для backend-разработчика на Go понимание планировщика важно при:

  • настройке GOMAXPROCS (по умолчанию - число ядер; в контейнерах с ограничением CPU нужно учитывать, что Go может видеть больше ядер, чем доступно)
  • диагностике дедлоков и гонок
  • оптимизации latency-sensitive сервисов: блокирующие операции (например, file I/O) могут приводить к переключению M, что увеличивает накладные расходы
  • использовании blocking-вызовов в библиотеках (например, cgo) - они могут занять M и потребовать создания нового потока
  • понимании поведения при высоком конкурентном трафике: планировщик не гарантирует fairness, но work-stealing минимизирует дисбаланс

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

  • не создавать горутины в бесконечном цикле без ограничения - это приведёт к росту потребления памяти
  • использовать worker pool или errgroup для ограничения параллелизма
  • избегать блокирующих syscall в горячем пути - лучше использовать неблокирующие версии (например, net.Conn работает через netpoller)
  • при использовании runtime.GOMAXPROCS(1) планировщик работает как cooperative, что может быть полезно для отладки

Пример кода

GO
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
runtime.GOMAXPROCS(2) // ограничим число P для наглядности
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// точка переключения: канал или sleep
time.Sleep(10 * time.Millisecond)
fmt.Printf("goroutine %d done\n", id)
}(i)
}
wg.Wait()
// Пример с runtime.Gosched - явная уступка
go func() {
for i := 0; i < 3; i++ {
runtime.Gosched()
fmt.Println("yielded goroutine")
}
}()
time.Sleep(50 * time.Millisecond)
}

Как отвечать на собеседовании

Начни с краткого определения: горутины + M:N планирование. Затем опиши три сущности G, M, P и их взаимодействие. Упомяни, что планировщик - preemptive с work-stealing, и приведи примеры точек переключения. Если спросят про производительность - объясни, почему горутины дешевле потоков (маленький стек, переключение в userspace). Хорошо добавить, что GOMAXPROCS влияет на параллелизм, а не на конкурентность. Если интервьюер углубляется - расскажи про syscall-handler, netpoller и отличие от потоков ОС.

Что проверяет интервьюер

  • понимание разницы между конкурентностью и параллелизмом
  • знание внутреннего устройства рантайма (G, M, P)
  • понимание, когда происходит переключение контекста и как планировщик избегает блокировок
  • способность объяснить trade-off между горутинами и потоками
  • практическое понимание GOMAXPROCS и его влияния на производительность
  • умение диагностировать проблемы (например, почему горутина не завершается или почему 100k горутин - это нормально)

Типичные ошибки

  • утверждение, что горутины - это потоки ОС (это не так, они выполняются на потоках)
  • мнение, что планировщик полностью кооперативный (с Go 1.14 он preemptive)
  • игнорирование роли P: без P горутина не может выполняться
  • непонимание, что GOMAXPROCS ограничивает число одновременно выполняющихся горутин, а не общее их количество
  • утверждение, что горутины всегда дешевле потоков - при большом количестве блокирующих syscall накладные расходы на создание M могут быть значительными
  • забывают про глобальную очередь и work-stealing - это ключевые механизмы балансировки нагрузки

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью