cult3

Using Tasks in Elixir

Aug 03, 2016

Table of contents:

  1. Starting a Task
  2. Async and Await
  3. Conclusion

A couple of weeks ago we looked at working with processes in Elixir. To create a new process we can use the spawn/1 function. This creates a new process and executes the given function concurrently to the current process.

Elixir also provides the Task module that builds upon these basic spawn functions to provide convenience methods and better error reports.

Tasks can also be supervised as part of supervision trees. We haven’t looked into supervision trees yet, so we won’t touch upon that aspect of Tasks in this tutorial. However, we will be taking a deeper look into supervision trees in a future tutorial.

Instead, today lets take a look at using Tasks in Elixir.

Starting a Task

The easiest way to get started with Tasks is to use the start/1 function:

{:ok, pid} = Task.start(fn -> 2 * 2 end)

If you read Working with Processes in Elixir, you can think of this function as essentially the same as the spawn/1 function.

This will create a new process and return the pid. However, when using the Task module, the pid will be wrapped in a tuple, where the first element is the atom :ok.

As with the spawn/1 function, this is fine if you don’t really care about the result. For example, if you just wanted to fire and forget you could use this function.

The Task module also has the start_link/1 function which, like the spawn_link/1 function, will create a processes where if there is a crash, that crash will be propagated to the original process.

We’re not going to be looking into supervising process in this tutorial so I will leave it at that.

In a future tutorial we are going to be going into much more depth into this topic as it’s a really interesting aspect of Elixir and Erlang.

Async and Await

Whilst sometimes it is fine to just fire and forget, you will often want to get a result back from running the task. For example, if you want to move a piece of sequential code to become concurrent by computing a value asynchronously.

To do that we can use the async/1 and await/1 functions

First we can use the async/1 function to create a new process and execute a function, just like we saw with the start/1 function in the last section:

task = Task.async(fn -> 2 * 2 end)

However, unlike the start/1 function from the last section, the return value of the async function is a %Task struct (What are Elixir Structs?):

# %Task{pid: #PID<0.63.0>, ref: #Reference<0.0.3.77>}

This struct can then be passed to the await/1 function to return the computed value from the async/1 function:

Task.await(task)

You can now move time consuming work into a Task and then grab the result when you need it:

# Compute time consuming work
task = Task.async(fn -> time_consuming_work() end)

# Continue working...

# Get the result
result = Task.await(task)

Processes created with the async are linked to the caller by default. This means if the caller process crashes so to will the spawned process.

Conclusion

The Task module builds upon the basic spawn functions to provide convenience functions and better error reporting.

This allows you to create tasks that can be linked together or move code from running sequentially and allow it to run concurrently instead.

Elixir has a number of these helper modules that make working with processes easier. We will continue to explore working with processes over the next couple of weeks.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.