当前位置: 首页>>代码示例 >>用法及示例精选 >>正文


Elixir Task用法及代码示例


Elixir语言中 Task 相关用法介绍如下。

产生和等待任务的便利。

任务是旨在在其整个生命周期中执行一项特定操作的进程,通常与其他进程很少或根本没有通信。任务最常见的用例是通过异步计算值将顺序代码转换为并发代码:

task = Task.async(fn -> do_some_work() end)
res = do_some_other_work()
res + Task.await(task)

使用async 生成的任务可以由它们的调用者进程(并且只有它们的调用者)等待,如上面的示例所示。它们是通过生成一个进程来实现的,该进程在执行给定的计算后向调用者发送消息。

除了 async/1 await/2 之外,任务还可以作为监督树的一部分启动并在远程节点上动态生成。接下来我们将探讨这些场景。

异步和等待

任务的常见用途之一是使用 Task.async/1 将顺序代码转换为并发代码,同时保持其语义。调用时,调用者将创建、链接和监视一个新进程。任务操作完成后,将向调用者发送一条带有结果的消息。

Task.await/2 用于读取任务发送的消息。

使用 async 时需要考虑两件重要的事情:

  1. 如果您使用的是异步任务,则必须等待回复,因为它们是 always 发送的。如果您不期待回复,请考虑使用 Task.start_link/1 ,详情如下。

  2. 异步任务链接调用者和生成的进程。这意味着,如果调用者崩溃,任务也会崩溃,反之亦然。这是有目的的:如果要接收结果的过程不再存在,那么完成计算就没有目的了。

    如果不希望这样做,您将需要使用监督任务,如下所述。

动态监督任务

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 模块。

分布式任务

由于 Elixir 提供了 Task.Supervisor ,因此很容易使用它来跨节点动态启动任务:

# On the remote node
Task.Supervisor.start_link(name: MyApp.DistSupervisor)

# On the client
supervisor = {MyApp.DistSupervisor, :remote@local}
Task.Supervisor.async(supervisor, MyMod, :my_fun, [arg1, arg2, arg3])

请注意,在处理分布式任务时,应该使用需要显式模块、函数和参数的 Task.Supervisor.async/5 函数,而不是使用匿名函数的 Task.Supervisor.async/3 。这是因为匿名函数期望所有涉及的节点上都存在相同的模块版本。查看 Agent 模块文档以获取有关分布式进程的更多信息,因为那里说明的限制适用于整个生态系统。

静态监督任务

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)

由于这些任务是受监督的,并且不直接与调用者相关联,因此无法等待它们。默认情况下,函数 Task.start/1 Task.start_link/1 用于fire-and-forget 任务,您不关心结果或它是否成功完成。

use Task 定义了一个 child_spec/1 函数,允许将定义的模块放在监督树下。生成的 child_spec/1 可以使用以下选项进行自定义:

  • :id - 子规范标识符,默认为当前模块
  • :restart - 当孩子应该重新启动时,默认为 :temporary
  • :shutdown - 如何立即关闭孩子,或者让孩子有时间关闭

GenServer Agent Supervisor 相对,任务的默认 :restart:temporary 。这意味着即使任务崩溃也不会重新启动任务。如果您希望在不成功退出时重新启动任务,请执行以下操作:

use Task, restart: :transient

如果您希望任务始终重新启动:

use Task, restart: :permanent

有关更多详细信息,请参阅 Supervisor 模块中的"Child specification" 部分。紧接在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") 从 Process 字典中检索当前进程的调用者列表。这将返回 nil 或带有至少一个条目的列表 [pid_n, ..., pid2, pid1] 其中 pid_n 是调用当前进程的 PID,pid2 调用 pid_npid2pid1 调用。

如果任务崩溃,调用者字段将作为日志消息元数据的一部分包含在 :callers 键下。

相关用法


注:本文由纯净天空筛选整理自elixir-lang.org大神的英文原创作品 Task。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。