0 Преамбула
Модель – это ещё не мир. Являясь людьми, мы не можем в полной мере познать реальность. Мы можем лишь построить её модель и через неё изучать и использовать реальный мир. От того, какую модель мы выберем, зависит полнота, успешность, живучесть части реальности в информационном пространстве (или в нашей голове).
У каждого языка программирования своя парадигма построения реальности. В функциональных языках процесс вычисления трактуется как вычисление значений функций, в императивных языках, наоборот, вычислительный процесс описывается в виде инструкций, изменяющих состояние программы.
В данной статье автор осветит функциональный язык программирования Erlang, парадигма которого может звучать так: «все является процессами». В первой части данной стати будет дана вводная информация по созданию и коммуникации процессов между собой, во второй мы остановимся на планировании процессов внутри виртуальной машины Erlang и спецификации процессов. Статья адресована для новичков, кто хочет начать создавать сложные, многопоточные и отказоустойчивые приложения на языке Эрланг.
1 Работа с процессами
В языке Erlang реализована легковесная модель процессов, которые запускаются в виртуальной машине — BEAM (Bogdan/Björn’s Erlang Abstract Machine), данная модель позволяет:
- быстро создавать, уничтожать процессы;
- единственный способ взаимодействия между процессами – это через передачу сообщений;
- процессы не разделяют память и являются полностью независимыми;
- можно создавать огромное количество процессов;
- легкое масштабирование приложений на SMP.
1.1 Создание процессов
Для создания процессов используются следующие функции (через слэш указано количество аргументов, которые может принимать функция):
- erlang:spawn/1/2/3/4 – создание обычного процесса;
- erlang:spawn_link/1/2/3/4 – создание процесса и связывание его с вызывающим процессом;
- erlang:spawn_monitor/1/3;
- pool:pspawn/3 – создание процесса на одном из узлов (минимально загруженном) в пуле;
- pool:pspwn_link/3 – аналогично предыдущему пункту, только создается связь между вызывающим процессом и создаваемым.
Помимо этих функций в документации [1] можно найти множество функций, предназначенных для обслуживания и манипуляции процессами.
Теперь давайте создадим простой процесс и запустим его. Запускаем интерактивную оболочку и выполняем следующие команды:
Eshell V5.8 (abort with ^G)
1> Fun = fun() -> receive after infinity -> ok end end.
#Fun<erl_eval.20.67289768>
2> Pid = spawn(Fun).
<0.33.0>
3>
В первой строке мы создаем функцию, которая будет являться телом процесса, затем создаем сам процесс используя функцию spawn/1, процесс создан и ему присвоен идентификатор <0.33.0>. Теперь вызовем функцию c:i/0 (c – это модуль, в котором собраны функции для интерактивной работы в оболочке Эрланга):
3> i().
Pid Initial Call Heap Reds Msgs
Registered Current Function Stack
<0.0.0> otp_ring0:start/2 987 2581 0
...
<0.33.0> erlang:apply/2 233 18 0
erl_eval:receive_clauses/8 10
Видим, что наш процесс работает. Так же, для мониторинга процессов, можно запустить графическую утилиту pman:
5> pman:start().
<0.37.0>
6>
1.2 Коммуникация между процессами
Теперь давайте рассмотрим самый простой пример того, как процессы могут общаться между собой. Для этого напишем небольшой модуль (для написания и отладки программ на Эрланге автор использует связку Emacs + Erlang mode + distel):
- -module(proc).
- -export([start/0, p/0]).
-
- start() ->
- spawn(proc, p, []).
-
- p() ->
- receive
- {Pid, Msg} when is_pid(Pid) ->
- io:format("Hello from proc: ~p, mesg: ~p~n", [Pid, Msg]),
- Pid ! Msg,
- p();
- stop ->
- ok;
- _ ->
- io:format("Unknown type of message~n", []),
- p()
- end.
Структура модуля достаточно проста, в 1-ой строке описывается имя модуля, оно должно совпадать с названием файла (в данном случае файл именуется proc.erl), во 2-ой строке мы экспортируем две функции, первая предназначена для создания процесса, а вторая описывает тело функции. Для создания процесса мы используем версию функции spawn с тремя аргументами, их можно описать кортежем {M, F, A}, где M – это модуль, в котором находится функция F, вызываемая со списком аргументов A (в данном случае у функции нет аргументов). Для тела процесса используется конструкция:
Receive
Pattern1 when Guard1 -> exp-11,...exp-1n;
...
Pattern1 when Guard1 -> exp-m1,...exp-mn
after Time -> exp-k1,...exp-kh
End
Где Pattern – шаблон для сопоставления полученного сообщения, Guard дополнительные условия, Time – таймер, указывается в мс, срабатывает если очередь пуста в течении заданного времени.
Теперь скомпилируем наш модуль и попробуем создать процесс.
1> c("proc").
{ok,proc}
2> Pid = proc:start().
<0.37.0>
3> Pid ! {self(), "Hello from shell"}.
Hello from proc: <0.30.0>, mesg: "Hello from shell"
{<0.30.0>,"Hello from shell"}
4> flush().
Shell got "Hello from shell"
ok
5>
В первой строке мы компилируем наш модуль, затем создаем процесс. В переменной Pid хранится идентификатора процесса которому с помощью оператора !, посылается сообщение в виде кортежа {self(),"..."}, где функция self/0 возвращает идентификатор процесса, в котором мы находимся – процесс интерактивная оболочка.
Созданный процесс, получив сообщение, достает его из очереди и сопоставляет с одним из шаблонов в данном случае с {Pid, Msg}, после чего отправляет полученное сообщение обратно. Функция flush/0 в данном случае нужна, чтобы сбросить все сообщения, отправленные интерактивной оболочке, это нужно, поскольку данный процесс не имеет блока receive.
В качестве практики предлагаю читателям создать два независимых процесса, которые обмениваются процессами, например, по команде из эраланговского shell-a первый посылает пару чисел второму, который суммирует их и отправляет назад первому, а он уже выводит ответ в shell.
2 Копаем глубже
Давайте рассмотрим ещё несколько вопросов, касающихся процессов: во-первых как работает планировщик процессов, и во-вторых, посмотрим, какую информацию можно узнать о процессе.
2.1 Планировщик процессов
Планирование эрланговских процессов основано на редукциях. Редукция в логике и математике — логико-методологический приём сведения сложного к простому (Википедия). Одна редукция примерно эквивалента вызову функции. Процессу разрешается выполняться пока он не будет приостановлен в ожидании ввода (сообщения от другого процесса, в данном случае приостановленный процесс находится в блоке receive) или пока не израсходует N редукций (с точным числом не уверен, но где-то находил, что оно равно 1000 редукций).
Существуют функции с помощью которых можно влиять на процесс планирования (erlang:yield/0, erlang:bump_reductions/1), но применять их следует только в редких случаях (как говорится в документации[1] данные функции могут быть изменены/удалены в следующих релизах). Процесс, ожидающий сообщение будет перепланирован, как только появится сообщение в его очереди или сработает таймер в блоке receive, после чего он помещается последним в очередь планировщика.
В эрланге есть 4 очереди с разными приоритетами: максимальный (max), высокий (high), нормальный (normal) и низкий (low). Планировщик сначала будет искать процессы в очереди с приоритетом max и запускать их, пока очередь не опустеет, затем тоже самое с процессами в high очереди. Затем, при условии, что max и high процессов больше нет, планировщик будет запускать процессы с приоритетом normal, до тех пор, пока очередь не опустеет, или пока процесс не выполнит определенное число редукций, после чего планировщик обработает процессы с приоритетом low.
Приоритет normal и low могут меняться местами, например: у вас сотни процессов с приоритетом normal и несколько с low, в данном случае планировщик может сначала выполнить процессы с приоритетом low и только затем с normal.
2.2 Внутренняя информация о процессе
Среди множества функций для работы с процессами есть одна очень интересная: erlang: process_info/1 или erlang: process_info/2, с помощью данной функции можно получить детализированную информацию о словаре процесса, сборщике мусора, размере кучи, прилинкованных процессах и другие интересные вещи (полный список смотрите в документации [1]).
Первый вариант функции (с одним аргументом) рекомендуется использовать только для отладочных целей – она выдает полную спецификацию процесса, для всего остального лучше использовать второй вариант.
Давайте напишем простую функцию, которая будет выводить следующую информацию о процессе: зарегистрированное имя; функция, по средством которой, процесс был порожден; список прилинкованных процессов.
- -module(test).
- -export([info/1]).
-
- info(Pid) ->
- Spec = [registered_name, initial_call, links],
- case process_info(Pid, Spec) of
- undefined ->
- undefined;
- Result ->
- [{pid, Pid}|Result]
- end.
В 5 строке мы создаем спецификацию, согласно которой будет получена информация о процессе. Запускаем шелл, компилируем модуль и тестируем.
3> processes().
[<0.0.0>,<0.3.0>,<0.5.0>,<0.6.0>,<0.8.0>,<0.9.0>,<0.10.0>,
<0.11.0>,<0.12.0>,<0.13.0>,<0.14.0>,<0.15.0>,<0.16.0>,
<0.17.0>,<0.18.0>,<0.19.0>,<0.20.0>,<0.21.0>,<0.23.0>,
<0.24.0>,<0.25.0>,<0.26.0>,<0.30.0>]
4> test:info(pid(0,0,0)).
[{pid,<0.0.0>},
{registered_name,init},
{initial_call,{otp_ring0,start,2}},
{links,[<0.5.0>,<0.6.0>,<0.3.0>]}]
5>
В 3 строке мы получаем список всех запущенных процессов, затем вызываем нашу функцию, которая показывает, что зарегистрированное имя процесса <0.0.0> — init, он порождён вызовом otp_ring0:start/2 и к нему прилинкованы три других процесса.
В следующей статье мы рассмотрим, как связывать друг с другом процессы и отслеживать их состояние.
Список литературы
1. Отличная интерактивная документация.
2. Базовые сведения о процессах.
3. На передовой дизайна виртуальных машин.
4. ERLANG Programming by Francesco Cesarini and Simon Thompson
5. Про планирование процессов.