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}" |
The call to Task.async creates a separate process that runs the given function. The return value of async is a task descriptor (actually a PID and a ref) that we’ll use to identify the task later.
Once the task is running, the code continues with other work. When it wants to get the function’s value, it calls Task.await, passing in the task descriptor. This call waits for our background task to finish and returns its value.
When we run this, we see
| $ elixir tasks1.exs |
| Start the task |
| Do something else |
| Wait for the task |
| The result is 6765 |
We can also pass Task.async the name of a module and function, along with any arguments. Here are the changes:
| worker = Task.async(Fib, :of, [20]) |
| result = Task.await(worker) |
| IO.puts "The result is #{result}" |
Tasks are implemented as OTP servers, which means we can add them to our application’s supervision tree. We can do this in a number of ways.
First, we can link a task to a currently supervised process by calling start_link instead of async. This has less impact than you might think. If the function running in the task crashes and we use start_link, our process will be terminated immediately. If instead we use async, our process will be terminated only when we subsequently call await on the crashed task.
The second way to supervise tasks is to run them directly from a supervisor. Here we specify the Task module itself as the module to run, and pass it the function to be run in the background as a parameter.
| children = [ |
| { Task, fn -> do_something_extraordinary() end } |
| ] |
| |
| Supervisor.start_link(children, strategy: :one_for_one) |
You can take this approach a step further by moving the task’s code out of the supervisor and into its own module.
| defmodule MyApp.MyTask do |
| use Task |
| |
| def start_link(param) do |
| Task.start_link(__MODULE__, :thing_to_run, [ param ]) |
| end |
| |
| def thing_to_run(param) do |
| IO.puts "running task with #{param}" |
| end |
| end |
The key thing here is use Task. This defines a child_spec function, allowing this module to be supervised:
| children = [ |
| { MyApp.MyTask, 123 } |
| ] |
The problem with this approach is that you can’t use Task.await, because your code is not directly calling Task.async.
The solution to this is to supervise the tasks dynamically. This is similar in concept to using a :simple_one_for_one supervisor strategy for regular servers. See the Task documentation for details.[37]
However, before you get too carried away, remember that a simple start_link in an already-supervised process may well be all you need.