Параллелизм
Одна из сильных сторон Elixir — поддержка параллелизма. Благодаря Erlang VM (BEAM) параллелизм в Elixir легче, чем вы думали. В основе модели параллелизма лежат акторы — процессы, взаимодействующие с другими процессами путём передачи сообщений.
В этом уроке мы познакомимся с модулями параллелизма, поставляемыми вместе с Elixir В следующей части мы также узнаем, каким способом они реализованы в OTP.
Процессы
Процессы в Erlang VM легковесны и выполняются на всех процессорах. Они могут показаться похожими на нативные потоки, но они проще, и вполне нормально иметь тысячи параллельных процессов в одном приложении Elixir.
Простейший способ создать новый процесс это функция spawn
, принимающая анонимную или именованную функцию.
Когда мы создаём новый процесс, он возвращает Идентификатор процесса, или PID, для однозначного определения внутри нашего приложения.
Для начала создадим модуль и опишем функцию, которую мы хотели бы запустить:
defmodule Example do
def add(a, b) do
IO.puts(a + b)
end
end
iex> Example.add(2, 3)
5
:ok
Чтобы выполнить функцию асинхронно, воспользуемся spawn/3
:
iex> spawn(Example, :add, [2, 3])
5
#PID<0.80.0>
Передача сообщений
Для взаимодействия между собой процессы используют сообщения.
Для этого существует две части: send/2
и receive
.
Функция send/2
позволяет отправлять сообщения PID’y.
Для получения и проверки сообщений используется receive
.
Если при проверке совпадение не будет найдено, выполнение продолжится.
defmodule Example do
def listen do
receive do
{:ok, "hello"} -> IO.puts("World")
end
listen()
end
end
iex> pid = spawn(Example, :listen, [])
#PID<0.108.0>
iex> send pid, {:ok, "hello"}
World
{:ok, "hello"}
iex> send pid, :ok
:ok
Стоит заметить, что функция listen/0
рекурсивна (вызывает саму себя), что позволяет этому процессу обработать несколько сообщений.
Без этого вызова процесс завершит свою работу после обработки первого сообщения.
Связывание процессов
Одна из проблем при использовании spawn
— узнать о выходе процесса из строя.
Для этого мы свяжем наши процессы с помощью spawn_link
.
Два связанных процесса будут получать друг от друга уведомления о завершении:
defmodule Example do
def explode, do: exit(:kaboom)
end
iex> spawn(Example, :explode, [])
#PID<0.66.0>
iex> spawn_link(Example, :explode, [])
** (EXIT from #PID<0.57.0>) evaluator process exited with reason: :kaboom
Иногда мы не хотим, чтобы связанный процесс завершал текущий.
Для этого нужно перехватывать попытки завершения с помощью функции Process.flag/2
.
Она использует функцию process_flag/2 Erlang с флагом trap_exit
. Если перехват включён (trap_exit
равно true
), перехваченные попытки будут получены в виде сообщения-кортежа: {:EXIT, from_pid, reason}
.
defmodule Example do
def explode, do: exit(:kaboom)
def run do
Process.flag(:trap_exit, true)
spawn_link(Example, :explode, [])
receive do
{:EXIT, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
end
end
end
iex> Example.run
Exit reason: kaboom
:ok
Мониторинг процессов
Но что делать, если мы не хотим связывать два процесса, но при этом хотим получать информацию? Можно воспользоваться spawn_monitor
для мониторинга процесса.
При наблюдении за процессом мы получаем сообщения, если процесс выйдет из строя, без завершения текущего процесса и необходимости явно перехватывать попытки завершения.
defmodule Example do
def explode, do: exit(:kaboom)
def run do
spawn_monitor(Example, :explode, [])
receive do
{:DOWN, _ref, :process, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
end
end
end
iex> Example.run
Exit reason: kaboom
:ok
Агенты
Агенты — абстракция над фоновыми процессами, сохраняющими состояние. Мы можем получить доступ к ним из другого процесса нашего приложения. Состояние агента устанавливается равным возвращаемому значению нашей функции:
iex> {:ok, agent} = Agent.start_link(fn -> [1, 2, 3] end)
{:ok, #PID<0.65.0>}
iex> Agent.update(agent, fn (state) -> state ++ [4, 5] end)
:ok
iex> Agent.get(agent, &(&1))
[1, 2, 3, 4, 5]
Если мы зададим имя агенту, то сможем обращаться к нему, используя имя, а не PID:
iex> Agent.start_link(fn -> [1, 2, 3] end, name: Numbers)
{:ok, #PID<0.74.0>}
iex> Agent.get(Numbers, &(&1))
[1, 2, 3]
Задачи
Задачи предоставляют возможность выполнять функцию в фоновом режиме и получать её значение потом. Они могут быть особенно полезны при обработке дорогостоящих операций без блокировки выполнения приложения.
defmodule Example do
def double(x) do
:timer.sleep(2000)
x * 2
end
end
iex> task = Task.async(Example, :double, [2000])
%Task{
owner: #PID<0.105.0>,
pid: #PID<0.114.0>,
ref: #Reference<0.2418076177.4129030147.64217>
}
# Делаем что-нибудь
iex> Task.await(task)
4000
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!