DEV Community

Felipe Araujo
Felipe Araujo

Posted on • Edited on

The Power of Elixir Task Module - The beginning

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)
Enter fullscreen mode Exit fullscreen mode
{:ok, #PID<0.114.0>}
Enter fullscreen mode Exit fullscreen mode

Task.async

Task.async(fn -> IO.inspect("Hello") end)
Enter fullscreen mode Exit fullscreen mode
%Task{
  owner: #PID<0.110.0>,
  pid: #PID<0.118.0>,
  ref: #Reference<0.626386777.2138832899.106529>
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 on Task.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}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)