Для упрощения работы с процессами, в Elixir есть модули Task
и Agent
. Для начала познакомимся с Task
.
Преимущества модуля Task
в отличие от создания процессов напрямую через spawn
заключаются в улучшенном выводе ошибок, а так же соответствующим API для обработки таких задач:
Task.start(fn -> raise "boom" end)
# => {:ok, #PID<0.8444.0>}
# => 16:20:15.164 [error] Task #PID<0.8444.0> started from #PID<0.105.0> terminating
# => ** (RuntimeError) boom
# => iex:368: (file)
# => (elixir 1.15.0) src/elixir.erl:374: anonymous fn/4 in :elixir.eval_external_handler/1
# => Function: #Function<43.3316493/0 in :erl_eval.expr/6>
# => Args: []
Task.start_link(fn -> raise "boom" end)
# => {:ok, #PID<0.8446.0>}
# проверим и очистим почтовый ящик процесса
flush()
# => {:EXIT, #PID<0.8446.0>,
# => {%RuntimeError{message: "boom"},
# => [
# => {:elixir_eval, :__FILE__, 1, [file: ~c"iex", line: 372]},
# => {:elixir, :"-eval_external_handler/1-fun-2-", 4,
# => [file: ~c"src/elixir.erl", line: 374, error_info: %{module: Exception}]}
# => ]}}
Помимо улучшенного отчета об ошибке в модуле Task
есть API для асинхронной обработки задач с помощью функций Task.async()
и Task.await()
:
tasks = 0..5 |> Enum.map(fn x -> Task.async(fn -> x * 2 end) end)
# => [
# => %Task{
# => mfa: {:erlang, :apply, 2},
# => owner: #PID<0.105.0>,
# => pid: #PID<0.8447.0>,
# => ref: #Reference<0.73723457.2325282820.199341>
# => },
# => %Task{
# => mfa: {:erlang, :apply, 2},
# => owner: #PID<0.105.0>,
# => pid: #PID<0.8448.0>,
# => ref: #Reference<0.73723457.2325282820.199342>
# => },
# => .........
# => ]
tasks |> Enum.map(fn task -> Task.await(task) end)
# => [0, 2, 4, 6, 8, 10]
В модуле есть и другие функции, с которыми потом можно самостоятельно ознакомиться.
Теперь рассмотрим модуль Agent
подробнее. Если нужно создать процесс, который хранит состояние и это состояние необходимо передавать другим процессам или состояние используется одним процессом в течении некоторого времени - в этом случае подходит Agent
. Примеры:
defmodule Counter do
use Agent
def start_link(initial_state \\ 0) do
Agent.start_link(fn -> initial_state end, name: __MODULE__)
end
def current_value do
Agent.get(__MODULE__, fn state -> state end)
end
def inc do
Agent.update(__MODULE__, fn state -> state + 1 end)
end
def dec do
Agent.update(__MODULE__, fn state -> state - 1 end)
end
end
Counter.start_link(2)
# => {:ok, #PID<0.115.0>}
Counter.current_value()
# => 2
Counter.inc()
# => :ok
Counter.current_value()
# => 3
Counter.dec()
# => ok
Counter.current_value()
# => 2
Так как агент запущен в отдельном процессе, счетчик может безопасно изменяться при конкурентной обработке.
В функции start_link
происходит глобальная регистрация процесса-агента и его связывание с процессом из которого вызвана функция, подобно функции spawn_link
. Про регистрацию процессов будет рассказано в следующих упражнениях.
В функции current_value
происходит обращение к внутреннему состоянию процесса и возврат этого состояния наружу.
В функциях inc
и dec
изменяется состояние процесса-агента, увеличиваясь или уменьшаясь на единицу соответственно.
Модуль Agent
предполагает разделение на клиентскую и серверную часть. Функции, переданные в качестве аргументов для модуля Agent
вызываются внутри агента (серверная часть). В ином случае вызов кода считается клиентской частью. Рассмотрим подробнее на примере:
# вычисление произойдет на стороне процесс-агента, то есть на серверной части
def do_something(agent) do
Agent.get(agent, fn state -> run_slow_code(state) end)
end
# вычисление произойдет на стороне процесса, вызвавшего функцию, то есть на клиентской части
def do_something(agent) do
Agent.get(agent, fn state -> state end) |> run_slow_code()
end
Первая функция блокирует работу агента. Вторая функция копирует все состояние на клиента и затем выполняет операцию на клиенте. При этом следует учитывать, достаточно ли велики данные, чтобы выполнить их обработку на сервере, или их можно быстро переслать клиенту. Другой фактор - нужно ли обрабатывать данные атомарно: получение состояния и вызов run_slow_code(state) вне агента означает, что состояние агента может быть обновлено в это время. Это особенно важно в случае обновлений, поскольку вычисление нового состояния на клиенте, а не на сервере может привести к условиям гонки, если несколько клиентов будут пытаться обновить одно и то же состояние на разные значения.
В модуле Agent
есть и другие функции, которые потом можно самостоятельно изучить.
После объявления модуля агента, в нем еще появляется функция child_spec/1
, про ее смысл поговорим дальше, в рамках дерева супервизии процессов.
В этот раз допишите функции агента Accumulator
, используя модуль Task
и опираясь на модуль Calculator
:
add
прибавить к состоянию аккумулятора переданное число;sub
вычесть состояние аккумулятора на переданное число;mul
умножить состояние аккумулятора на переданное число;div
разделить состояние аккумулятора на переданное число;reset
сбрасывает состояние Accumulator
в ноль;current
возвращает нынешнее состояние Accumulator
.Accumulator.start_link(0)
Accumulator.add(10) # => :ok
Accumulator.current() # => 10
Accumulator.sub(2) # => :ok
Accumulator.current() # => 8
Accumulator.mul(2) # => :ok
Accumulator.current() # => 16
Accumulator.div(4) # => :ok
Accumulator.current() # => 4
Accumulator.reset() # => :ok
Accumulator.current() # => 0
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1defmodule Test do
2 use ExUnit.Case
3
4 describe "calculator code unchanged" do
5 test "adding" do
6 assert Calculator.exec(:+, 2, 3) == 5
7 assert Calculator.exec(:+, 10, 20) == 30
8 end
9
10 test "subtraction" do
11 assert Calculator.exec(:-, 2, 3) == -1
12 assert Calculator.exec(:-, 10, 20) == -10
13 end
14
15 test "multiply" do
16 assert Calculator.exec(:*, 2, 3) == 6
17 assert Calculator.exec(:*, 10, 20) == 200
18 end
19
20 test "division" do
21 assert Calculator.exec(:/, 2, 3) == 0
22 assert Calculator.exec(:/, 20, 10) == 2
23 end
24 end
25
26 test "accumulator agent" do
27 Accumulator.start_link(0)
28
29 Accumulator.add(10)
30 assert Accumulator.current() == 10
31 Accumulator.add(3)
32 assert Accumulator.current() == 13
33
34 Accumulator.mul(10)
35 assert Accumulator.current() == 130
36 Accumulator.mul(3)
37 assert Accumulator.current() == 390
38
39 Accumulator.sub(10)
40 assert Accumulator.current() == 380
41 Accumulator.sub(3)
42 assert Accumulator.current() == 377
43
44 Accumulator.div(10)
45 assert Accumulator.current() == 37
46 Accumulator.div(3)
47 assert Accumulator.current() == 12
48
49 Accumulator.reset()
50 assert Accumulator.current() == 0
51 assert Agent.stop(Accumulator) == :ok
52 end
53end
54
Решение учителя откроется через: