The Power of Elixir Task Module - The beginning
Table of Contents
Intro
Recently I was studying more deeply how the elixir Task module works and to consolidate my study I decided to write this post.
Before we start, let’s check the definition of the Task module. The best place to do it is in the Elixir official documentation. There we have:
Conveniences for spawning and awaiting tasks.
Tasks are processes meant to execute one particular action throughout their lifetime, often with little or no communication with other processes. The most common use case for tasks is to convert sequential code into concurrent code by computing a value asynchronously
Good definition, but show me the code!!
Start an async operation
We have two basic ways to execute an async operation using Task
. It’s possible to use Task.start
and Task.async
. Let’s see how it works.
Task.start
Task.start(fn -> IO.inspect("Hello") end)
{:ok, #PID<0.114.0>}
Task.async
Task.async(fn -> IO.inspect("Hello") end)
%Task{
owner: #PID<0.110.0>,
pid: #PID<0.118.0>,
ref: #Reference<0.626386777.2138832899.106529>
}
It’s possible to see that Task.start
returns a tuple with :ok
and a process id, while Task.async
returns a Task struct. However, both work in the same way.
Sometimes we need to wait some async process result to execute the next step. Let’s do it!
Waiting for results
The previous sample was very basic. Adding some delay will make it better.
Task.async(fn ->
:timer.sleep(5000)
IO.inspect("Hello")
:ok
end)
As we know, the response will be a %Task{}
struct. To await for a response we have two options: Task.await
and Task. yield
. Let’s check their differences:
Task.await
- Default timeout is 5 seconds;
- Given a timeout, it throws an exception;
- After the timeout is reached, the task is stopped;
- You can define a custom timeout or use the atom
:infinity
.
Task.await(task)
Task.await(task, :infinity)
Timeout sample
> task = Task.async(fn -> IO.inspect("Hello") ; :timer.sleep(10000); :ok end)
> Task.await(task)
"Hello"
** (exit) exited in: Task.await(%Task{owner: #PID<0.110.0>, pid: #PID<0.124.0>, ref: #Reference<0.3761442499.262406148.76432>}, 5000)
** (EXIT) time out
(elixir 1.11.3) lib/task.ex:643: Task.await/2
As we can see, a timeout is a little explosive when using Task.await
. A way to handle it better is to use Supervised Tasks.
Task.yield
- Default timeout is 5 seconds;
- Given a timeout, it returns
nil
; - Using the atom
:infinity
is not allowed as onTask.await
; - After the timeout is reached, it keeps the task running;
- It’s possible to finish a running task using
Task.shutdown(task, shutdown \\ 5000)
.
> task = Task.async(fn -> IO.inspect("Hello") ; :timer.sleep(10000); :ok end)
> Task.yield(task)
nil
# Let's check again
> Task.yield(task)
{:ok, :ok}
Given a :timeout
result, the response will be nil
. After that, we can execute Task.yield
again. To avoid long running tasks without any results, you have to use Task.shutdown(task, shutdown \\ 5000)
.
A more complete sample
We have a list of items and it’s necessary to execute some work in all of them:
items = ["alpha", "beta", "gama"]
Enum.map(items, fn item ->
Task.async(fn ->
:timer.sleep(4000)
IO.inspect("Hello #{item}")
:ok
end)
end)
|> Enum.map(&Task.await/1)
|> function_to_handle_results()
With this approach, we still have the exception given a timeout is reached. However, it’s possible to handle it in a better way with Supervised Tasks, but this subject will be covered in the next post.
Additional content
Originally published on dev.to