We have looked at two extremes for processes:
What if we want something more in the middle?
We are going to talk about:
An Elixir task is a function that runs in the background.
defmodule Fib do
def of(0), do: 0
def of(1), do: 1
def of(n), do: Fib.of(n-1) + Fib.of(n-2)
end
IO.puts "Start the task"
worker = Task.async(fn -> Fib.of(20) end)
IO.puts "Do something else"
# ...
IO.puts "Wait for the task"
result = Task.await(worker)
IO.puts "The result is #{result}"
Task.async creates a separate process that runs the given function. The return value of async is a task descriptor. To get the function’s value, it calls Task.await
$ elixir tasks1.exs
Start the task
Do something else
Wait for the task
The result is 6765
worker = Task.async(Fib, :of, [20])
result = Task.await(worker)
IO.puts "The result is #{result}"
We can also pass Task.async the name of a module and function, along with any arguments.
When we run the code:
Tasks are implemented as OTP servers, which means we can add them to our application’s
supervision tree in two ways.
# 2 directly in supervisor
children = [
{ Task, fn -> do_something_extraordinary() end }
]
Supervisor.start_link(children, strategy: :one_for_one)
1st way: If the function running in the task crashes and we use start_link, our process will be terminated immediately. If we use async, our process will be terminated only when we subsequently call await.
An agent is a background process that maintains state. This state can be accessed at different places within a process or node, or across multiple nodes.
It's initial state is set by a function
we pass in when we start it.
Agent.get will get the state.
Agent.update will change the state.
iex> { :ok, count } = Agent.start(fn -> 0 end)
{:ok, #PID<0.69.0>}
iex> Agent.get(count, &(&1))
0
iex> Agent.update(count, &(&1+1))
:ok
iex> Agent.update(count, &(&1+1))
:ok
iex> Agent.get(count, &(&1))
2
We can give an agen a name using name:.
iex> Agent.start(fn -> 1 end, name: Sum)
{:ok, #PID<0.78.0>}
iex> Agent.get(Sum, &(&1))
1
iex> Agent.update(Sum, &(&1+99))
:ok
iex> Agent.get(Sum, &(&1))
100
We exploit the fact that an uppercase bareword in Elixir is converted into an atom with the prefix Elixir., so when we say Sum it is actually the atom :Elixir.Sum.
Encapsulate an agent inside a module.
defmodule Frequency do
def start_link do
Agent.start_link(fn -> %{} end, name: __MODULE__)
end
def add_word(word) do
Agent.update(__MODULE__,
fn map ->
Map.update(map, word, 1, &(&1+1))
end)
end
def count_for(word) do
Agent.get(__MODULE__, fn map -> map[word] end)
end
def words do
Agent.get(__MODULE__, fn map -> Map.keys(map) end)
end
end
Encapsulate an agent inside a module.
iex> c "agent_dict.exs"
[Frequency]
iex> Frequency.start_link
{:ok, #PID<0.101.0>}
iex> Frequency.add_word "dave"
:ok
iex> Frequency.words
["dave"]
iex(41)> Frequency.add_word "was"
:ok
iex> Frequency.add_word "here"
:ok
iex> Frequency.add_word "he"
:ok
iex> Frequency.add_word "was"
:ok
iex> Frequency.words
["he", "dave", "was", "here"]
iex> Frequency.count_for("dave")
1
iex> Frequency.count_for("was")
2
defmodule Dictionary do
@name __MODULE__
# External API
def start_link,
do: Agent.start_link(fn -> %{} end, name: @name)
def add_words(words),
do: Agent.update(@name, &do_add_words(&1, words))
def anagrams_of(word),
do: Agent.get(@name, &Map.get(&1, signature_of(word)))
# Internal implementation
defp do_add_words(map, words),
do: Enum.reduce(words, map, &add_one_word(&1, &2))
defp add_one_word(word, map),
do: Map.update(map, signature_of(word), [word], &[word|&1])
defp signature_of(word),
do: word |> to_charlist |> Enum.sort |> to_string
end
defmodule WordlistLoader do
def load_from_files(file_names) do
file_names
|> Stream.map(fn name -> Task.async(fn -> load_task(name) end) end)
|> Enum.map(&Task.await/1)
end
defp load_task(file_name) do
File.stream!(file_name, [], :line)
|> Enum.map(&String.trim/1)
|> Dictionary.add_words
end
end
# four wordlist file
list1 list2 list3 list4
angor ester palet rogan
argon estre patel ronga
caret goran pelta steer
carte grano petal stere
cater groan pleat stree
crate leapt react terse
creat nagor recta tsere
creta orang reest tepal
$ iex anagrams.exs
iex> Dictionary.start_link
{:ok, #PID<0.66.0>}
iex> Enum.map(1..4, &"words/list#{&1}") |> WordlistLoader.load_from_files
[:ok, :ok, :ok, :ok]
iex> Dictionary.anagrams_of "organ"
["ronga", "rogan", "orang", "nagor", "groan", "grano", "goran",
"argon", "angor"]
defmodule Dictionary do
@name {:global, __MODULE__}
Agents and tasks run as OTP servers, so they are easy to distribute—just give our agent a globally accessible name.
# window 1
$ iex --sname one anagrams_dist.exs
iex(one@FasterAir)>
# Window 2
$ iex --sname two anagrams_dist.exs
iex(two@FasterAir)> Node.connect :one@FasterAir
true
iex(two@FasterAir)> Node.list
[:one@FasterAir]
# Window 2 - load up list3 and list4
iex(two@FasterAir)> WordlistLoader.load_from_files(~w{words/list3 words/list4})
[:ok, :ok]
# Window 1 - load up list1 and list2
iex(one@FasterAir)> Dictionary.start_link
{:ok, #PID<0.68.0>}
iex(one@FasterAir)> WordlistLoader.load_from_files(~w{words/list1 words/list2})
[:ok, :ok]
# Window 1
iex(one@FasterAir)> Dictionary.anagrams_of "argon"
["ronga", "rogan", "orang", "nagor", "groan", "grano", "goran", "argon",
"angor"]
# Window 2
iex(two@FasterAir)> Dictionary.anagrams_of "crate"
["recta", "react", "creta", "creat", "crate", "cater", "carte",
"caret"]
When do you use agents and tasks, and when do you use a GenServer?
Use the simplest approach that works.
Agents and tasks are great when you are dealing with very-specific background activities, whereas GenServers (as their name suggests) are more general.
You can eliminate the need to make a decision by wrapping your agents and tasks in modules, as we did in our anagram example. That way you can always switch from the agent or task implementation to the full-blown GenServer without affecting the rest of the code base.