用于方便的派生和等待异步任务。
任务是一个执行特定动作的进程,期间几乎不与其他进程通信。任务的常用场景是通过异步计算将顺序代码转化成并发代码。
task = Task.async(fn -> do_some_work() end)
res = do_some_other_work()
res + Task.await(task)
通过 async
派生的任务可以(也只能)由调用进程等待结果,如上例所示。其原理是派生进程会在计算完成时向调用者发送一个消息。
相比于通过 spawn/1
派生的进程,任务包含了监控和日志。
除了 async/1
和 await/2
,任务也可以做为监控树的一部分启动,以及动态在远程节点派生。稍后我们会介绍。
async和await
任务的一个常用场景是通过 Task.async/1
在不改变语义的情况下将顺序代码变成并发代码。它会创建一个被调用者链接和监控的新进程。当任务结束时,结果会通过消息发送给调用者。
Task.await/2
被用来读取任务发来的消息。
使用 async
有两点注意事项:
- 使用
aync
时,必须使用await
接收回复,因为它一定会被发送。如果不需要回复,可以使用Task.start_link
。 async
将调用者和任务进行链接了起来,这意味着如果调用者崩溃,任务也会崩溃,反之亦然。这是有意为之的:如果接收结果的进程不存在了,那么计算也就没必要进行了。如果这不是你想要的,请使用监督任务。
任务只是进程
任务其实就是进程,因此数据需要完整的复制给它。看下面的例子:
large_data = fetch_large_data()
task = Task.async(fn -> do_some_work(large_data) end)
res = do_some_other_work()
res + Task.await(task)
上面的代码需要拷贝完整的 large_data
,根据数据大小可能会占用许多资源。有两种方式来避免。
首先,如果你只需要访问 large_data
的一部分,那就没必要全部传过去:
large_data = fetch_large_data()
subset_data = large_data.some_field
task = Task.async(fn -> do_some_work(subset_data) end)
其次,你可以将加载数据也放到任务当中:
task = Task.async(fn ->
large_data = fetch_large_data()
do_some_work(large_data)
end)
动态监督任务
Task.Supervisor
模块允许开发者动态创建监督任务。
举个例子:
{:ok, pid} = Task.Supervisor.start_link()
task =
Task.Supervisor.async(pid, fn ->
# Do something
end)
Task.await(task)
当然,主流的做法是将任务监督者加到你的监督树中:
Supervisor.start_link([
{Task.Supervisor, name: MyApp.TaskSupervisor}
], strategy: :one_for_one)
然后你就可以通过监督者名称而不是 pid 来使用 async/await 了:
Task.Supervisor.async(MyApp.TaskSupervisor, fn ->
# Do something
end)
|> Task.await()
我们鼓励开发者尽量使用监督任务。监督任务提高了任一时刻有多少任务正在运行的可见性,而且在处理结果,错误,超时上给了你更多元化的控制。总结如下:
- 当你不关心结果甚至不关心是否运行成功时,使用
Task.Supervisor.start_child/2
开启一个即起即忘的任务。 - 使用
Task.Supervisor.async/2
加Task.await/2
允许你并发执行任务并获取结果。如果任务失败,调用者也会失败。 - 使用
Task.Supervisor.async_nolink/2
加Task.yield/2
加Task.shutdown/2
允许你并发执行任务并在给定时间内获取结果或者失败原因。如果任务失败,调用者不会失败。你会从yield
或shutdown
收到错误原因。
再者,当应用停止时,监督者保证了所有任务在一段时间内终止。详见 Task.Supervisor
。
分布式任务
使用 Task.Supervisor
,很容易在不同节点动态启动任务,
# First on the remote node named :remote@local
Task.Supervisor.start_link(name: MyApp.DistSupervisor)
# Then on the local client node
supervisor = {MyApp.DistSupervisor, :remote@local}
Task.Supervisor.async(supervisor, MyMod, :my_fun, [arg1, arg2, arg3])
注意,使用分布式任务时,需要使用 Task.Supervisor.async/5
函数指定模块,函数和参数,而不是 Task.Supervisor.async/3
使用匿名函数。这是因为匿名函数要求所有调用的节点存在相同版本的模块。
静态监督任务
Task
模块实现了 child_spec/1
函数,这让它可以直接在一个常规的 Supervisor
下启动,而不是 Task.Supervisor
。如下,在元组中指定要执行的函数:
Supervisor.start_link([
{Task, fn -> :some_work end}
], strategy: :one_for_one)
如果你需要在设置监督树时执行一些步骤,这会很有用。比如:预热缓存,记录初始状态,等等。
如果你不想将任务代码直接放到 Supervisor
下,你可以将 Task
封装到自己的模块中,就像使用 GenServer
和 Agent
一样:
defmodule MyTask do
use Task
def start_link(arg) do
Task.start_link(__MODULE__, :run, [arg])
end
def run(arg) do
# ...
end
end
然后将它传递给监督者:
Supervisor.start_link([
{MyTask, arg}
], strategy: :one_for_one)
由于这些任务被监督并且没有直接链接到调用者,因此也不能使用 await
。默认 Task.start/1
和 Task.start_link/1
用于即启即忘的任务,既不关心结果,也不关心是否成功。
当你使用
use Task
时,Task
模块会定义一个child_spec/1
函数,因此你的模块可以被用作监督树的子节点。
use Task
定义了一个 child_spec/1
函数,允许你的模块可以加入一个监督树。生成的 child_spec/1
函数可以通过以下选项自定义:
:id
- 子节点标识,默认是当前模块。:restart
- 当子节点挂了需要重启时,默认:temporary
。:shutdown
- 如何停止子节点,要么立即终止,要么给一个停止时间。
相比于 GenServer
, Agent
和 Supervisor
,Task 的 :restart
默认为 :temporary
。这意味着任务崩溃后不会重启。如果你希望任务在非正常退出时重启,需要这样:
use Task, restart: :transient
如果希望任务总是重启:
use Task, restart: :permanent
use Task
前面的 @doc
会添加到生成的 child_spec/1
函数。
祖先和调用者追踪
每当你启动一个新进程时,Elixir 通过进程字典里的 $ancestors
键记录该进程的父节点。它常被用来追踪监督树中的层级。
例如我们鼓励开发者总是在监督者下启动任务。这不仅提供了可见性,也让你可以在节点关闭时控制任务的终止。这看起来有点像 Task.Supervisor.start_child(MySupervisor, task_function)
。这意味着,尽管是你的代码调用了任务,但实际上任务的祖先是监督者,因为实际启动任务的是监督者。
为了追踪你的代码和任务之间的关系,我们可以使用进程字典中的 $callers
键。因此,假如像上面那样调用 Task.Supervisor
,我们有:
[your code] -- calls --> [supervisor] ---- spawns --> [task]
这意味着我们存储了以下关系:
[your code] [supervisor] <-- ancestor -- [task]
^ |
|--------------------- caller ---------------------|
当前进程的调用者列表可以使用 Process.get(:"$callers")
从进程字典中获取。它会返回 nil
或者 [pid_n, ..., pid2, pid1]
列表,该列表不会为空,至少有一项。 pid_n
是调用当前进程的进程PID, pid2
调用了 pid_n
,且被 pid1
调用。
如果任务崩溃,调用者字段会做为日志消息元数据的一部分出现在 :callers
下。