任务
了解 Elixir 中的任务及其监督机制。
本课程的这一部分讨论的是进程及其分布。到目前为止,我们已经了解了两个极端情况:在早期章节中,我们学习了 spawn 原语,以及消息发送与接收和多模式操作;之后,我们又研究了 OTP——进程架构的主导框架。
然而,有时我们想要介于两者之间的一种方案。我们希望能够运行简单的进程,用于后台处理或状态维护,但又不想被 spawn、send 和 receive 这些底层细节所困扰,也并不需要自定义 GenServer 所带来的额外控制能力。
这时,任务(tasks) 和 代理(agents) 就派上用场了。它们是 Elixir 中简单易用的抽象机制,利用了 OTP 的特性,同时屏蔽了这些底层细节。
任务(Tasks)
Elixir 中的 任务 是一个在后台运行的函数。
1defmodule Fib do 2 def of(0), do: 0 3 def of(1), do: 1 4 def of(n), do: Fib.of(n-1) + Fib.of(n-2) 5end 6 7IO.puts "开始执行任务" 8worker = Task.async(fn -> Fib.of(20) end) 9IO.puts "执行其他工作" 10## ... 11IO.puts "等待任务完成" 12result = Task.await(worker) 13 14IO.puts "结果是 #{result}"
调用 Task.async 会创建一个独立的进程来运行指定的函数。async 的返回值是一个任务描述符(实际上是 PID 和引用),我们稍后可以用它来标识这个任务。
任务启动后,代码将继续执行其他工作。当需要获取函数的返回值时,调用 Task.await,传入任务描述符。该调用会阻塞,直到后台任务完成,并返回其结果。
我们也可以将模块名、函数名及参数传递给 Task.async。以下是相应修改:
1worker = Task.async(Fib, :of, [20]) 2result = Task.await(worker) 3IO.puts "结果是 #{result}"
任务与监督
任务是基于 OTP 服务器实现的,这意味着我们可以将其加入应用程序的监督树中。有多种方式可以做到这一点。
第一种方式是通过调用 start_link 而不是 async,将任务链接到当前受监督的进程。这种方式的影响可能比想象中要小。如果任务中的函数发生崩溃,使用 start_link 会导致我们的进程立即终止;而使用 async 时,只有当我们后续对崩溃的任务调用 await 时,进程才会被终止。
第二种方式是直接让监督者启动任务。此时,我们将 Task 模块本身作为要运行的模块,并将要在后台执行的函数作为参数传入。
1children = [ 2 { Task, fn -> do_something_extraordinary() end } 3] 4Supervisor.start_link(children, strategy: :one_for_one)
我们还可以进一步优化,将任务的代码从监督者中移出,放到独立的模块中。
1defmodule MyApp.MyTask do 2 use Task 3 4 def start_link(param) do 5 Task.start_link(__MODULE__, :thing_to_run, [param]) 6 end 7 8 def thing_to_run(param) do 9 IO.puts "正在运行任务,参数为 #{param}" 10 end 11end
关键点在于 use Task。这会自动定义一个 child_spec 函数,使得该模块可以被监督:
1children = [ 2 { MyApp.MyTask, 123 } 3]
这种方法的问题是,我们无法再使用 Task.await,因为我们的代码并未直接调用 Task.async。
解决这个问题的方法是动态地监督任务。这在概念上类似于使用 :simple_one_for_one 监督策略来管理普通服务器。具体细节可参考任务文档。
不过,在实际应用中,请记住:在一个已受监督的进程中简单地使用 start_link,很可能已经足够满足需求。
请运行以下代码:
No configuration found for ID: 9ee2d6b4180b60f2d1e6c0cdd0f7b8e6661a60b76221da86a48d2207bc900798