Tasks

An Elixir task is a function that runs in the background.

tasks/tasks1.exs
 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:

tasks/tasks2.exs
 worker = Task.async(Fib, ​:of​, [20])
 result = Task.await(worker)
 IO.puts ​"​​The result is ​​#{​result​}​​"

Tasks and Supervision

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.

tasks/my_app/lib/my_app/my_task.ex
 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.