Working with Tasks in Elixir
Jack Marchant
Posted on July 26, 2018
While writing Understanding Concurrency in Elixir I started to grasp processes more than I have before. Working with them more closely has strengthened the concepts in my own mind.
In Elixir's standard library, there's a few modules that abstract common code that without these modules you'd find youself repeating often.
When you want to write asynchronous code, you may care about the result of the code, and sometimes you might not.
The Task module makes it easy, either way.
To better understand how tasks work, I thought I would create a simple (naive) module that would implement a similar API to that of the Task.
Re-implementing the Task module
Consider the following module, which I'm going to call Job
.
defmodule Job do
def async(fun) when is_function(fun) do
parent = self()
spawn_link(fn ->
send(parent, {self(), fun.()})
end)
end
def await(job, timeout \\ 5000) do
receive do
{^job, result} -> result
after
timeout -> {:error, "no result"}
end
end
end
Job.async/1
accepts a single function as a parameter, and this is the work that will be carried out asynchronously. You can either run the function, without caring about the result:
iex> Job.async(fn -> "Hi" end)
<#PID>
It returns a Process Identifier (PID), which is the result of calling spawn_link/1
, passing in a function which in turn sends a message to the parent process. We've split up the implementation of async
and await
so that you can optionalally pass the PID to await
if you care to wait for a result.
Let's see what that would look like:
iex> Job.async(fn -> "Hi" end) |> Job.await()
"Hi"
When we pattern match on the job PID to identify the message being received, and the result of the job
, the value of the result is the result of invoking the function passed to Job.async/1
.
In this case the result was seen instantly, but if it the initial function was actually performing asynchronous work, then it would wait for a timeout period to elapse before giving up. This is the after
section of the await
function.
iex> Job.async(fn -> :timer.sleep(6000) end) |> Job.await(5000)
{:error, "no result"}
We got an error because the timeout had elapsed, given the timer in the function paused processing until 6 seconds had gone, whereas the Job.await/2
function gave up waiting after 5 seconds.
Conclusion
Hopefully the Job
module helps your understanding of what the Task
module is doing under the hood, to some degree, it is not the full implementation and there's a whole lot more that come with using tasks, such as process supervision, streaming, and more. That being said, it can be useful to become familiar with passing messages between processes, in any case.
Posted on July 26, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.