Лёгкие процессы и spawn

Главная суперсила Erlang: дешёвые изолированные процессы.

Процесс в Erlang — это независимый лёгкий поток выполнения со своей памятью, общающийся с другими только сообщениями. Это «актор» в модели акторов.

Мы подошли к сердцу Erlang. Всё, что мы изучали — типы, функции, рекурсия — было подготовкой. Настоящая мощь языка раскрывается в конкурентности, построенной на процессах. Именно ради неё Erlang в своё время и создавался: язык родился в телекоме, где система обязана одновременно обслуживать тысячи звонков, не давая сбою на одной линии повлиять на остальные. Отсюда и весь набор уже знакомых вам черт — неизменяемые данные, рекурсия вместо циклов, отсутствие общей памяти — складывается в стройную картину, как только мы добавляем процессы.

Стоит сразу развести два похожих слова. «Процесс» в Erlang — это не процесс операционной системы и не системный поток. Это сущность самой виртуальной машины BEAM, неизмеримо более лёгкая, чем потоки ОС. Поэтому привычные из других языков опасения — «потоки дороги, их надо беречь, синхронизация мучительна» — здесь почти не действуют. Erlang переворачивает картину: процессов может быть очень много, они дёшевы, а вместо общих переменных и блокировок используется передача сообщений.

Модель акторов простыми словами

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

У этой картинки есть глубокое практическое следствие. Поскольку у сотрудников нет общих бумаг, за которые они могли бы подраться, исчезает целый класс кошмаров параллельного программирования — гонки за данными, взаимные блокировки, тонкие ошибки синхронизации. В языках с общей изменяемой памятью именно эти проблемы отнимают больше всего сил и порождают самые неуловимые баги. Модель акторов устраняет их в корне: делить попросту нечего, а значит, и портить нечего. Координация остаётся, но она выражается явно — через сообщения, которые видно в коде, а не через незримые столкновения за общую ячейку памяти.

Создание процесса через spawn

Функция spawn запускает новый процесс, выполняющий заданную функцию, и возвращает его идентификатор — PID. Здесь важно прочувствовать смену перспективы: вы не «вызываете функцию и ждёте ответа», как привыкли, а «отпускаете функцию жить отдельно». Управление возвращается к вам мгновенно, а порождённый процесс начинает свой собственный путь параллельно с вами. То, что вы передаёте в spawn, — это анонимная функция из второго раздела курса; она становится «программой жизни» нового процесса.

Pid = spawn(fun() ->
    io:format("Я новый процесс!~n")
end).
% Печатает: Я новый процесс!
% Pid выглядит примерно так: <0.85.0>

PID (process identifier) — уникальный «адрес» процесса. По нему процессу отправляют сообщения, как по почтовому адресу шлют письма. Сам по себе PID ничего не «делает» — это просто ссылка, дескриптор, который вы храните, чтобы позже достучаться до процесса. Получив PID при создании процесса, вы обычно либо сразу шлёте на него сообщения, либо передаёте его другим процессам, чтобы и они могли связаться с этим работником.

Процессы независимы

Запущенный процесс живёт своей жизнью. Родитель не блокируется, ожидая его, — spawn возвращает управление немедленно. Каждый процесс имеет отдельную кучу: данные не разделяются. Эта изоляция — не просто удобство, а краеугольный камень надёжности Erlang. Раз память не общая, ошибка или даже аварийное падение одного процесса физически не может повредить данные другого. В худшем случае упавший процесс просто умирает, а остальные продолжают работать как ни в чём не бывало.

Именно на этой изоляции строится знаменитая отказоустойчивость языка, выраженная девизом «пусть падает» (let it crash). Вместо того чтобы обкладывать каждый шаг защитными проверками, в Erlang позволяют сбойному процессу честно упасть, зная, что взрыв локален и не заденет соседей, а специальный процесс-надзиратель при необходимости поднимет упавшего заново. Полностью эту схему мы разберём в разделе про OTP, но фундамент закладывается уже здесь, в самой идее изолированных процессов.

% Запускаем три независимых процесса
start_workers() ->
    spawn(fun() -> work(1) end),
    spawn(fun() -> work(2) end),
    spawn(fun() -> work(3) end),
    ok.

work(N) ->
    io:format("Работник ~p трудится~n", [N]).

Почему процессы такие дешёвые

Процесс Erlang начинается с микроскопического стека и кучи — порядка сотен байт. Создаётся за единицы микросекунд. Поэтому запустить процесс под каждое соединение, каждую задачу, каждый таймер — нормальная практика. В привычных языках «поток на запрос» — роскошь; в Erlang «процесс на что угодно» — рутина. Система спокойно держит сотни тысяч и миллионы процессов одновременно — масштаб, при котором потоки операционной системы давно бы исчерпали память.

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

Как работает под капотом

Когда вы вызываете spawn, BEAM выделяет новую структуру процесса с собственной кучей и стеком и ставит её в очередь планировщика. Процесс получает уникальный PID. Планировщик начнёт выполнять его, когда подойдёт очередь, честно деля процессорное время с остальными. Если процесс завершит свою функцию — он спокойно умирает, и память освобождается. Никакого «пула потоков» настраивать не нужно: масштабирование встроено в платформу.

Частые ошибки

  • Ждать, что spawn вернёт результат функции. Он возвращает PID, а не результат; результат передаётся сообщением.
  • Думать, что процессы делят переменные. У каждого своя память; связь только сообщениями.
  • Бояться создавать много процессов. Это дёшево и поощряется архитектурой Erlang.

Итоги

  • Модель акторов: независимые процессы общаются только сообщениями.
  • spawn запускает процесс и возвращает его PID — адрес для сообщений.
  • Процессы изолированы: у каждого своя память, общих переменных нет.
  • Процессы дёшевы — миллионы на машине — поэтому «процесс на задачу» это норма.
Проверьте себя
1. Что возвращает функция spawn?
AРезультат выполнения функции
BPID — идентификатор нового процесса
CСписок процессов
DАтом ok всегда
2. Как процессы Erlang обмениваются данными?
AЧерез общие переменные
BТолько сообщениями, потому что память изолирована
CЧерез файлы на диске
DЧерез глобальные блокировки
3. Почему в Erlang нормально создавать множество процессов?
AПроцессы хранятся в облаке
BОни очень лёгкие: мало памяти и быстрое создание
CИх число ограничено десятком
DОни не занимают память