Elixir 1.6
Chapter 22
Tasks and Agents
We have looked at two extremes for processes:
- spawn primitive (tiny)
- OTP (huge)
What if we want something more in the middle?
We are going to talk about:
- Tasks
- Agents
Tasks
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
Tasks
$ 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 and Supervision
Tasks are implemented as OTP servers, which means we can add them to our application’s
supervision tree in two ways.
- Call start_link instead of async
- Run them directly from a supervisor
# 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.
Agents
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
Agent Naming
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.
Agents
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
Agents
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"]
Agents and Tasks
defmodule Dictionary do
@name {:global, __MODULE__}
Make It Distributed
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]
Make It Distributed
# 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"]
Agents and Tasks, or GenServer?
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.
Thank you!
Programming Elixir 1.2 Chapter 22
By Dustin McCraw
Programming Elixir 1.2 Chapter 22
- 981