← назад к разделу

КАК УСТРОЕН КОМПЬЮТЕР Фаза 2 · Основы языка: Go

Процессы, потоки, память и диск

Go · backend

Что происходит между строкой Go-кода и работой железа и почему тысячи ожидающих запросов не требуют тысяч потоков операционной системы.

1. Программа использует четыре разных ресурса

1 / 7

Процессор вычисляет, память хранит данные работающей программы, диск сохраняет их после выключения, а сеть связывает процессы на разных машинах.

  • Код и входные данные сначала попадают в оперативную память.
  • CPU исполняет инструкции и обращается к памяти через кэши процессора.
  • Чтение с диска и ответ по сети - ожидание внешнего устройства, а не вычисление.
Go-программаHTTP, расчёты, сохранение CPUисполняет инструкции RAMданные работающей программы Дискданные после перезапуска Сетьбаза и другие сервисы

2. Процесс - изолированное рабочее место программы

2 / 7

Операционная система запускает программу как процесс, выделяет ресурсы и не даёт ему напрямую читать память другого процесса.

  • У процесса своё виртуальное адресное пространство и открытые файлы.
  • Один и тот же виртуальный адрес у двух процессов ведёт в разные места RAM.
  • Запрос к другому процессу идёт через ОС, сеть или разделяемую память.
Процесс AGo-сервисадрес 0x1000 Процесс BPostgreSQLадрес 0x1000 ОС и таблицы страницвиртуальный → физический RAM: ARAM: B Память изолирована, несмотря на одинаковые адреса

3. Поток - путь выполнения внутри процесса

3 / 7

Несколько потоков одного процесса делят код, кучу и открытые соединения, но у каждого свой стек и текущая инструкция.

  • Планировщик ОС по очереди даёт потокам время на доступных ядрах.
  • На N ядрах одновременно исполняется не больше N потоков.
  • Общие данные удобны для обмена, но требуют синхронизации.
Один процесс Поток 1свой стек Поток 2свой стек Общие код и кучанужна синхронизация Ядро CPU 1инструкцииЯдро CPU 2инструкции ОС выбирает, какой поток получит ядро следующим

4. Горутин много, потоков ОС мало

4 / 7

Горутина - задача рантайма Go, а не поток ОС. Планировщик Go распределяет множество горутин по меньшему числу системных потоков.

  • Ожидание канала или сети приостанавливает горутину и освобождает место для другой.
  • Вычисления всё равно упираются в число доступных процессоров.
  • Блокирующий системный вызов может занять поток ОС - рантайм учитывает это.
ГорутиныG1 · G2 · G3 · G4G5 · G6 · G7 · G8G ожидают сетьи каналы Планировщик Goочереди и переключениеGOMAXPROCSлимит параллельного Go-кода Поток ОС M1Поток ОС M2 Количество ожидающих задач не равно количеству потоков

5. Стек хранит вызовы, куча - переживающие их значения

5 / 7

Стек движется вместе с вызовами функций. Объекты кучи живут, пока на них можно добраться по ссылкам из работающей программы.

  • У каждой горутины собственный стек, который Go умеет увеличивать.
  • Куча общая: ссылку на её значение можно передать другой горутине.
  • Сборщик мусора освобождает только недостижимые значения.
Стек горутиныhandle(): аргументыmain(): указатель Общая кучаOrderпереживает вызовбез ссылок → мусор В Go размещение выбирает компилятор, а не оператор new

6. Пока один запрос ждёт, другой может работать

6 / 7

Медленная база не должна останавливать весь сервис. Ожидающая ввода-вывода горутина уступает место готовой вычисляться.

  • Сетевое ожидание Go обычно обрабатывает через сетевой поллер ОС.
  • Файловый вызов может блокировать поток ОС, но не весь планировщик.
  • Таймаут и отмена нужны, чтобы ожидание не длилось бесконечно.
ABCPU запрос в БДожидание ответа с таймаутомответ готоваобрабатывает другой HTTP-запрос CPU выполняет B, пока A ждёт Конкурентность помогает ожиданию, но не создаёт новые ядра

7. От железа к диагностике Go-сервиса

7 / 7

Симптом сначала связывают с ресурсом, затем подтверждают профилем. Не наоборот: случайная оптимизация без измерения редко помогает.

  • CPU и долгие вычисления - профиль profile и метрики процессора.
  • Рост памяти - heap, ссылки на объекты и лимиты процесса.
  • Много ожидающих задач - goroutine, таймауты и состояние внешних систем.
Высокий CPUсчитает, а не ждёт Растёт RAMкто держит ссылки Растут горутиныгде они заблокированы pprof/profilepprof/heappprof/goroutine Замер → причина → исправление → новый замер

Когда HTTP-запрос обрабатывается медленно, причина может быть совсем не в Go. Возможно, процессор занят вычислениями, значения заполнили память, база медленно читает диск или удалённый сервис не отвечает. Чтобы отличить эти случаи, нужна простая модель компьютера: без неё горутины, сборка мусора и профили выглядят как отдельные заклинания.

Что делает железо

Процессор исполняет машинные инструкции: складывает числа, сравнивает значения, переходит к другой инструкции. Компилятор Go превращает исходный код именно в такие инструкции. Несколько ядер позволяют процессору действительно выполнять несколько последовательностей инструкций одновременно.

Оперативная память, RAM, хранит код и данные, к которым процессор обращается прямо сейчас: аргументы функций, срезы, map, сетевые буферы. Её содержимое исчезает после выключения питания. У процессора есть ещё более быстрые кэши, но программист на Go обычно не управляет ими напрямую.

Диск сохраняет файлы и данные базы после перезапуска. Обращение к нему требует работы устройства и операционной системы. Часть файлов ОС кэширует в RAM, поэтому повторное чтение может обойтись без физического обращения к диску.

Сеть передаёт байты другому процессу, часто на другой компьютер. Запрос в PostgreSQL или HTTP-вызов не являются обычным чтением из памяти: после отправки данных программа ждёт работу внешней системы и обратную передачу ответа.

Практический вывод: нагрузка на процессор и ожидание ввода-вывода - разные проблемы. Добавление горутин не ускоряет вычисления сверх возможностей ядер, но позволяет использовать процессор, пока часть задач ждёт диск или сеть.

Зачем нужна операционная система

Программа не работает с железом совершенно самостоятельно. Операционная система распределяет процессор и память, управляет файлами и сетевыми соединениями, а также изолирует программы друг от друга.

Запущенная программа называется процессом. ОС даёт ему виртуальное адресное пространство: адреса в Go-указателях не являются напрямую номерами ячеек физической RAM. ОС и процессор отображают виртуальные страницы на физические и проверяют права доступа.

Поэтому Go-сервис и PostgreSQL могут использовать одинаковое число как виртуальный адрес, но попадут в разные участки памяти. Ошибка в одном процессе обычно не позволяет ему просто прочитать или перезаписать память другого. Для обмена нужны механизмы ОС: сокеты, каналы, файлы или явно настроенная разделяемая память.

Процесс - это единица владения ресурсами и изоляции. Но внутри него должна существовать хотя бы одна последовательность исполняемых инструкций. Ею занимается поток.

Что такое поток

Поток ОС - последовательность выполнения, которую планирует ядро операционной системы. У потока есть текущая инструкция, состояние регистров и собственный стек вызовов. Потоки одного процесса используют общую кучу, код и открытые файловые дескрипторы.

Если два потока одновременно меняют одну структуру данных, без синхронизации результат может быть повреждён. Общая память не становится безопасной от того, что оба потока принадлежат одному процессу. В Go для координации применяют каналы, примитивы sync и атомарные операции.

Ядер меньше, чем готовых работать потоков, поэтому ОС переключает их. На двух ядрах два потока могут выполняться параллельно, а остальные в этот момент ждут своей очереди, завершения ввода-вывода или снятия блокировки.

Создание и переключение потоков ОС требует участия ядра, а каждый системный стек резервирует заметный объём памяти. Создавать отдельный поток на каждый из десятков тысяч преимущественно ожидающих HTTP-запросов расточительно. Go решает это на уровне своего рантайма.

Как горутины связаны с потоками

Горутина - легковесная единица выполнения Go. Она имеет состояние и стек, но не закреплена за одним потоком ОС навсегда. Рантайм распределяет горутины по системным потокам и ставит на выполнение те, которые готовы продолжаться.

Когда горутина ждёт канал, таймер или сетевой ответ, она приостанавливается. Другая готовая горутина получает возможность выполняться. Поэтому большое число ожидающих запросов само по себе не означает такое же число потоков ОС.

Важны две оговорки:

  • Долгие вычисления не превращаются в ожидание. Если всем горутинам нужен процессор, их пропускная способность ограничена доступными ядрами и GOMAXPROCS.
  • Обычный файловый ввод-вывод и вызовы внешнего C-кода могут блокировать поток ОС. Рантайм может отделить право выполнять другие горутины от такого потока, но не отменяет стоимость самого блокирующего вызова.

В следующей статье модель разбирается через G, M и P. Здесь достаточно запомнить: горутины - задачи, потоки ОС - исполнители, ядра - физический ресурс.

Стек и куча

При вызове функции ей нужны аргументы, локальные значения и адрес возврата. Эти данные образуют кадр вызова в стеке. При возврате кадр больше не нужен. У каждой горутины свой стек: Go начинает с небольшого размера и увеличивает его по мере необходимости.

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

Не нужно считать, что new всегда означает кучу, а обычная локальная переменная всегда стек. Решение принимает компилятор после анализа времени жизни значения. Размещение в куче само по себе не является ошибкой; важна стоимость аллокаций на действительно горячем пути.

Куча общая для горутин процесса. Сборщик мусора начинает с корней - стеков и глобальных переменных - и идёт по ссылкам. Недостижимые значения можно освободить. Если забытая горутина хранит ссылку на большой объект, этот объект остаётся живым: сборщик не угадывает, что он больше не нужен по смыслу.

Почему сервис ждёт

Предположим, обработчик отправил SQL-запрос. Пока база читает данные и сеть переносит ответ, процессору нечего вычислять именно для этого запроса. Если поток просто спит, а других исполнителей нет, ядро простаивает.

Go старается не тратить это время: ожидающая горутина снимается с выполнения, а другая обрабатывает следующий запрос. Сетевой поллер сообщает рантайму, когда соединение снова готово. Это делает конкурентную модель особенно полезной для backend-систем, где много времени уходит на базы, очереди и удалённые API.

Ожидание всё равно нужно ограничивать. context.WithTimeout и настройки клиентов задают границу, после которой запрос отменяется. Иначе внешний сбой накапливает горутины, соединения и память, а затем выглядит уже как проблема самого сервиса.

Отмена не заменяет устранение медленной зависимости. Она защищает ресурсы и возвращает управление вызывающему, а профили и метрики помогают найти источник задержки.

Как это увидеть в работающем сервисе

Начинайте с вопроса, какой ресурс исчерпан или чего ждёт программа:

НаблюдениеЧто проверитьИнструмент
Высокая загрузка CPUКакие функции выполняют вычисленияМетрики CPU, pprof/profile
Растёт память процессаКакие объекты достижимы и кто их создаётpprof/heap, gctrace
Растёт число горутинНа каком канале, блокировке или вызове они стоятpprof/goroutine
Медленные ответы при свободном CPUБазу, диск, сеть, очереди и таймаутыТрассировка и метрики зависимостей

Системные утилиты показывают процесс целиком, а pprof - внутреннее состояние Go. Эти уровни дополняют друг друга: высокий RSS процесса не обязан означать большую живую кучу Go, а тысяча горутин не обязана означать тысячу потоков ОС.

Коротко

  • CPU исполняет инструкции, RAM хранит данные работающей программы, диск обеспечивает постоянное хранение, сеть связывает процессы.
  • Процесс - изолированное адресное пространство и набор ресурсов; ОС отображает виртуальную память процессов на физическую RAM.
  • Поток ОС - путь выполнения с собственным стеком; потоки одного процесса делят кучу и должны синхронизировать доступ к общим данным.
  • Горутина - задача рантайма Go. Множество горутин выполняется на меньшем числе потоков ОС, а ожидание позволяет переключиться на готовую работу.
  • Стек хранит вызовы и локальные значения, куча - значения, которые должны жить дольше. В Go размещение выбирает компилятор, а недостижимую память освобождает сборщик.
  • Ожидание диска или сети и загрузка CPU - разные причины медленной работы. Таймауты ограничивают ожидание, профили показывают его источник.

Что почитать дальше