Pull to refresh

Erlang и его процессы

Reading time7 min
Views18K

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):
  1. -module(proc).
  2. -export([start/0, p/0]).
  3.  
  4. start() ->
  5.   spawn(proc, p, []).
  6.  
  7. p() ->
  8.   receive
  9.     {Pid, Msg} when is_pid(Pid) ->
  10.       io:format("Hello from proc: ~p, mesg: ~p~n", [Pid, Msg]),
  11.       Pid ! Msg,
  12.       p();
  13.     stop ->
  14.       ok;
  15.     _ ->
  16.       io:format("Unknown type of message~n", []),        
  17.       p()
  18.   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]).

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

Давайте напишем простую функцию, которая будет выводить следующую информацию о процессе: зарегистрированное имя; функция, по средством которой, процесс был порожден; список прилинкованных процессов.

  1. -module(test).
  2. -export([info/1]).
  3.  
  4. info(Pid) ->  
  5.   Spec = [registered_name, initial_call, links],
  6.   case process_info(Pid, Spec) of
  7.     undefined ->
  8.       undefined;
  9.     Result ->
  10.       [{pid, Pid}|Result]
  11.   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. Про планирование процессов.
Tags:
Hubs:
Total votes 47: ↑45 and ↓2+43
Comments23

Articles