Task
Usually, you wouldn’t work with spawn/1
directly but use the Task
module instead. It offers a range of helper functions for spawning one-off tasks.
# Use `Task.start/1` instead of `spawn/1` to start an unlinked process
# and `Task.start_link/1` instead of `spawn_link/1` to start a linked one.
fun = fn ->
{:ok, pid} =
Task.start(fn ->
IO.inspect("Parent Task started!")
Task.start_link(fn -> raise "BOOM!" end)
:timer.sleep(1)
IO.inspect("Parent Task is still alive!") # <- Won't execute
end)
IO.inspect("Parent started at #{inspect(pid)}")
:timer.sleep(1)
IO.inspect("Still alive 😎")
end
fun.()
"Parent started at #PID<0.232.0>"
"Parent Task started!"
** (RuntimeError) BOOM!
"Still alive 😎"
Task.async/1
and Task.await/1
A big advantage of using Task
over spawn/1
is that you can easily await the result of the async process using the common async/await
notation.
fun = fn ->
pid =
Task.async(fn ->
IO.inspect("Task starts work", label: NaiveDateTime.utc_now())
:timer.sleep(500)
:some_result
end)
IO.inspect("Starting other work", label: NaiveDateTime.utc_now())
:timer.sleep(200) # <- Do some other work
IO.inspect("Finished other work", label: NaiveDateTime.utc_now())
# Whenever you're ready, you can await the result.
result = Task.await(pid, :timer.seconds(1)) # Wait up to 1 second
IO.inspect("Task returned: #{result}", label: NaiveDateTime.utc_now())
end
fun.()
2024-08-31 15:35:17.500386: "Starting other work"
2024-08-31 15:35:17.500489: "Task starts work"
2024-08-31 15:35:17.700461: "Finished other work"
2024-08-31 15:35:18.000527: "Task returned: some_result"
Keep in mind that calling await/1
in the awaiting process will block the process until the awaited process returns a value or until the timeout is reached (5 seconds by default). If the timeout is reached, both processes will die 💀
Task.async_stream/3
You can easily apply an operation to a list of elements and collect the result in parallel by using Task.async_stream/3
.
elements = ["elixir", "is", "great"]
# Count the characters in each word and return the overall sum.
#
# Task.async_stream/3 runs as many tasks as you have schedulers in parallel.
# This defaults to the number of available CPUs. Each task applies
# the function to only one element and returns the result.
elements
|> Task.async_stream(fn word -> String.length(word) end)
|> Enum.reduce(0, fn {:ok, count}, acc -> count + acc end)
13
Take a second and look at this again.
In just 3 lines of code, we parallelised a task over all available CPUs! 🤯