Intro
If you just jump to Elixir from other languages, process & supervisor is one of many things you need to understand to take advantage of Elixir but it's hard for people they don't come from concurrent programming languages.
For newbie can work easy with process, Elixir provides Task
& Task.Supervisor
to help us work with Elixir process.
Task
Task
is high level abstract to work with process. We can create a process to execute a task and get result without boilerplate code (for spawn & get result).
Not same as GenServer
(much more simpler than GenServer
) Task
provides a simple way to execute a function (anonymous function or define in a module). We must care our state & loop function if need a long-run task. For state we have Agent module, you can check in this topic
Task
is simple for using both cases, using directly or define a module with Task
.
For start directly we can use like:
# create a task in other process.
task = Task.async(fn ->
# execute a task.
end)
# do something.
# get result
result = Task.await(task)
We can use await_many
for case we need to wait two or more tasks.
We can see a flow to create & wait a task:
We can see to execute a function as a process and get result is only in two functions call.
Remember our task create by Task.async
will linked with current process (parent) then if task is crashed or current process is crashed other process will die follow (if we don't want a link, can use async_nolink
in Task.Supervisor
). The link in this case help us follow "fail fast/let it crash", we don't need to clean rest processes if one of linked process was crashed.
We can use task as a child in supervisor by simple use Task
in our module like:
defmodule PermanentTask do
use Task, restart: :permanent
def start_link(arg) do
Task.start_link(__MODULE__, :run, [arg])
end
def run(arg) do
# ...
end
end
Then we can add to our supervisor like:
Supervisor.start_link([
# other children in supervisor.
{PermanentTask, arg}
], strategy: :one_for_one)
As we see, if we need add a simple task then Task
can help us reduce complex things like self define a process or implement a GenServer
for supervisor (I have other posts for explaining GenServer and Supervisor).
Deep dive to Task module we see Task.async
function will return a reference for Task.await
can get result from message.
Nothing magic in here, Task module just wrap function need to run and spawn process with link + monitor. If we run a short task then get message from queue (if run iex in terminal, we can use flush
function to get all messages in queue), we will see two messages like:
{#Reference<0.0.13315.722909021.800391170.124665>, 6}
{:DOWN, #Reference<0.0.13315.722909021.800391170.124665>, :process,
#PID<0.105.0>, :normal}
A message with {#Ref...
is a message contain result (where Task.await
will get result out), one other is message with format {:DOWN, #Ref..., :process, #PID<0.105.0>, :normal}
is created by monitor process feature :normal
that mean our task run & completed (no error).
It's simple right?
Task
can return a stream by using async_stream
for case we need to work with stream.
For case we want to ignore we can call ignore/1
to unlink a running task.
For case we need check status of unlinked task we can use yield/2
& yield_many/2
.
Task.Supervisor
Elixir provides a supervisor for Task
to work & manage dynamically tasks (support to create remote tasks).
children = [
{Task.Supervisor, name: OurApp.TaskSupervisor}
]
Supervisor.start_link(children, strategy: :one_for_one)
And at runtime we can add async task like:
task = Task.Supervisor.async(OurApp.TaskSupervisor, fn ->
# execute a task.
end)
For case we need to create a lot of tasks we can use Task.Supervisor
with partition to avoid bottleneck.
For case we need spawn long-run processes and need to communicate between these, we can use Agent
module or Ets table.
Use cases
If you work with LiveView process, Task is so easy to create another process to do task and these're a couple, if LiveView or Task process is crash, other one will die. We don't need care to clean :).
# In LiveView process, wait to receive a task from user.
handle_event("run_task", params, socket) do
Task.async(fn ->
# run task in other process, result format {:task, result}
do_task(params)
end)
{:noreply, assign(socket, task_ref: ref}
end
# receive result from task process
handle_info({ref, {:task, result}}, socket) do
Process.demonitor(ref, [:flush])
# do somethings with result.
{:noreply, socket}
end
Conclusion
Task
is good for case run one process or group of processes in period of time and get result from that. It's convenient for us code with flow in our mind, don't need to stop and back to Application or Supervisor module to add child and handle result.
Top comments (0)