Процессы, потоки и goroutine

Процесс — экземпляр программы с собственным виртуальным адресным пространством и ресурсами ОС. Поток — исполняемый контекст внутри процесса. Goroutine — лёгкая задача, которой управляет рантайм Go: она не является потоком ОС и мультиплексируется на них.

Зачем это на интервью

Эта модель нужна, чтобы объяснить изоляцию сервисов, параллелизм, цену создания задач, отмену работы и диагностику зависаний. На Go-интервью часто проверяют, понимаете ли вы, где заканчивается планировщик рантайма и начинается планировщик ядра.

Минимум для E4

  • Различать адресное пространство и ресурсы процесса от стека и регистров потока.
  • Объяснять модель Go G-M-P: goroutine (G) исполняется потоком ОС (M), когда M удерживает логический процессор рантайма (P).
  • Знать, что общая память потоков и goroutine требует синхронизации; процессы по умолчанию не разделяют пользовательскую память.
  • Уметь назвать, когда нужен отдельный процесс, а когда достаточно goroutine или потока.

В Linux отдельную программу обычно запускают через fork/exec: fork создаёт дочерний процесс, а exec заменяет его образ. clone — низкоуровневый системный вызов с флагами: в зависимости от них новая задача может разделять с вызывающей адресное пространство, таблицу файлов, PID namespace и другие ресурсы либо не разделять их. Потоки одного процесса разделяют адресное пространство, открытые файлы и ряд других ресурсов, но имеют отдельные регистры, стек и идентификатор потока. Goroutine начинает с малого растущего стека, а её планирование и парковка — ответственность Go runtime.

Углубление для E5/Senior

Планировщик Go реализует M:N-мультиплексирование: множество G обслуживается набором M. Число P ограничено GOMAXPROCS; оно задаёт, сколько Go-кода может исполняться параллельно, но не является строгим потолком количества потоков ОС. Рантайм может создать дополнительные M для системных вызовов, cgo и служебной работы.

Блокирующий системный вызов может увести M в ядро. Рантайм старается освободить P, чтобы другие goroutine продолжили работу на другом M; детали зависят от вызова и платформы. Не следует считать это гарантией: неограниченные блокирующие операции всё равно ухудшают latency и усложняют shutdown.

Граница процесса полезна для отказоизоляции, отдельных прав, независимого обновления и несовместимых рантаймов. Она дороже передачи данных: IPC требует сериализации и явного протокола. Граница goroutine дешевле, но общая память делает race, утечки и отсутствие backpressure риском всего процесса. Проектируйте отмену через context.Context, ограничивайте конкуренцию и подтверждайте предположения pprof, trace и Linux-инструментами.

Ключевые понятия

  • Адресное пространство — отображение виртуальных адресов процесса; другой процесс не может читать его память без специально разрешённого механизма.
  • PID/TID — идентификатор процесса и потока в Linux. Поток виден ядру как планируемая задача.
  • Стек хранит кадры вызовов конкретного потока или goroutine; heap процесса обычно разделён между его потоками.
  • G-M-P — G выполняет пользовательский код, M соответствует потоку ОС, P содержит ресурсы исполнения Go-кода и локальную очередь runnable G.
  • Preemption — возможность остановить выполняющуюся задачу, чтобы дать время другой; у Go есть кооперативные и асинхронные механизмы вытеснения.
  • IPC — обмен между процессами: pipe, socket, shared memory, очередь сообщений; выбор определяет границы ошибок и стоимость копирования.

Типовые вопросы

  1. Чем процесс отличается от потока?
    • Процесс изолирует адресное пространство и ресурсы; поток — единица исполнения внутри процесса. Потоки дешевле взаимодействуют через общую память, но нуждаются в синхронизации.
  2. Чем goroutine отличается от потока ОС?
    • Goroutine создаётся и планируется рантаймом Go, занимает динамический небольшой стек и может быть припаркована без блокирования потока. Поток ОС планируется ядром и имеет более тяжёлые ресурсы.
  3. Что означает GOMAXPROCS?
    • Это число P и верхняя граница одновременного исполнения Go-кода, не количество goroutine и не точное число M. Значение нужно сверять с доступной CPU-квотой контейнера и нагрузкой.
  4. Зачем fork часто сопровождается exec?
    • fork создаёт дочерний процесс как копию текущего; exec заменяет его образ другой программой. Вместе это стандартный путь запуска отдельной команды в Unix.
  5. Почему запись в map из двух goroutine опасна?
    • Goroutine делят heap процесса; одновременная запись без синхронизации — data race и может привести к аварийному завершению. Нужны sync.Mutex, владение данными одной goroutine либо другой согласованный протокол.
  6. Когда запускать работу в новом процессе, а не в goroutine?
    • Когда требуются независимые права, лимиты и жизненный цикл, защита от падения/утечки памяти или отдельный исполняемый файл. Для обычной параллельной работы одного сервиса goroutine проще и дешевле.

Практика

  • Напишите программу, которая запускает 100 000 goroutine, ожидающих канал. Снимите runtime.NumGoroutine, RSS процесса и число потоков через ps -o pid,nlwp,rss,cmd -p <pid>; объясните, почему первые два счётчика не равны.
  • Запустите go tool trace для CPU-bound и I/O-bound нагрузки. Найдите состояния runnable, running и waiting и сопоставьте их с планированием CPU.
  • Реализуйте worker pool с context.WithCancel, ограничением очереди и WaitGroup. Критерий: при отмене не остаётся goroutine, а producer получает backpressure.
  • Создайте дочерний процесс через os/exec, передайте ему данные через stdin и проверьте обработку ненулевого exit code и deadline.

Частые ошибки и ловушки

  • Считать, что goroutine создаёт поток ОС или что много goroutine автоматически ускоряют CPU-bound вычисление.
  • Запускать goroutine без владельца, отмены и способа дождаться завершения: так возникают утечки и незакрытые ресурсы.
  • Передавать изменяемые данные между goroutine без правил владения и надеяться, что race detector заменяет синхронизацию в production.
  • Вызывать fork вручную из многопоточного Go-процесса: используйте os/exec; после fork безопасен лишь ограниченный набор действий до exec.
  • Путать параллелизм (одновременное выполнение на ядрах) с конкурентностью (структурирование нескольких независимых работ).

Связанные темы

Операционные системы · Планирование CPU и переключение контекста · IPC · Go · Linux и сети · Наблюдаемость

Источники

  • Документация Go: Go scheduler в исходном коде runtime, Package runtime и Data Race Detector.
  • The Go Programming Language Specification: разделы про goroutine и channels.
  • man 2 fork, man 2 execve, man 2 clone, man 7 pthreads.
  • Michael Kerrisk, The Linux Programming Interface, главы о процессах и потоках.