任务

任务 属于 Elixir

2 个练习

关于 任务

任务是用于执行某项特定操作的进程。它们通常不与其他进程通信,但可以把结果返回给启动它的进程。

任务常被用来并行处理工作。

async/await

要启动一个任务,可以使用 Task.async/1。它接受一个匿名函数作为实参,并在一个新进程中执行它,这个新进程与调用方进程相互链接。它返回一个 %Task{} 结构体。

要获取执行的结果,请把 %Task{} 结构体传给 Task.await/2。它会等待任务完成并返回其结果。第二个实参是超时时间,单位为毫秒,默认为 5000。可以把 :infinity 作为超时时间传入,让任务执行时不受超时限制。

task = Task.async(fn -> Enum.sum(0..999_999) end)
# => %Task{
#      owner: #PID<0.110.0>,
#      pid: #PID<0.119.0>,
#      ref: #Reference<0.3715954046.815529991.83334>
#    }

Task.await(task)
# => 499999500000

注意,在启动任务和等待任务之间,启动任务的进程不会被阻塞,可以执行其他操作。

任何用 Task.async/1 启动的任务都应当等待它完成,因为它会向调用方进程发送一条消息。对每个任务,Task.await/2 只能调用一次。

如果你只想为了副作用而启动任务,可以使用 Task.start/1 或 Task.start_link/1。Task.start/1 启动的任务不与调用方进程链接,而 Task.start_link/1 启动的任务与调用方进程链接。这两个函数都会返回一个 {:ok, pid} 元组。

超时

一种常见模式是先启动异步任务,再用 Enum.map/2 等待它们,如下所示:

inputs
|> Enum.map(&Task.async(fn -> function.(&1) end))
|> Enum.map(&Task.await(&1, 1_000))

需要注意的是,Task.await 是同步的,每调用一次,它的超时计时都从 0 开始。也就是说,以这种方式依次使用它时,超时时间会累加。

第一个被等待的任务,运行时间不会超过超时时间;但第二个被等待的任务能运行的时间,等于第一个任务运行的时间加上超时时间。

设想这样一种情况:每个任务的运行时间都比前一个稍长,但任务运行时长的差值小于等待的超时时间。

inputs = 1..10
function = fn n -> :timer.sleep(n * 900) && n end

我们可以使用 :timer.tc/1(一个 Erlang 函数)来测量这些任务要运行多久,单位是微秒。

:timer.tc(fn ->
  inputs
  |> Enum.map(&Task.async(fn -> function.(&1) end))
  |> Enum.map(&Task.await(&1, 1_000))
end)

# => {9004517, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}

9004517 微秒就是 9 秒,远大于给定的 1 秒(1000 毫秒)超时时间。

如果这正是你想要的行为,那很好,尽管用!如果不想这样,另一种选择是使用 Task.async_stream/3 或 Task.await_many/2。

Task.async_stream/3

Task.async_stream/3 让你可以用许多不同的输入运行同一个函数,每个输入各在一个任务中运行。这些任务与当前进程链接,就像用 Task.async/1 运行时一样。它返回一个流。

它接受一个超时时间,该超时时间对每个任务单独生效。设想用 Task.async_stream/3 处理同样的输入和同样的函数:

inputs
|> Task.async_stream(function, timeout: 1_000)
|> Enum.to_list()

# => ** (exit) exited in: Task.Supervised.stream(1000)
#       ** (EXIT) time out
#       (elixir 1.11.3) lib/task/supervised.ex:304: Task.Supervised.stream_reduce/7
#       (elixir 1.11.3) lib/enum.ex:3473: Enum.reverse/1
#       (elixir 1.11.3) lib/enum.ex:3066: Enum.to_list/1

如果你要用同一个函数处理不同的输入,Task.async_stream/3 非常合适。它接受其他一些有用的选项,从而提供了很强的控制力,比如同时运行多少个任务,以及任务超时时该怎么办。

如果你需要运行执行不同操作的任务,可能就需要改用 Task.await_many/2。

Task.await_many/2

Task.await_many/2 在 Elixir 1.11 中引入。它让你用一个共享的超时时间等待许多任务。设想用 Task.await_many/2 处理同样的输入和同样的函数:

inputs
|> Enum.map(&Task.async(fn -> function.(&1) end))
|> Task.await_many(1_000)

# => ** (exit) exited in: Task.await_many([...], 1000)
#        ** (EXIT) time out
#        (elixir 1.11.3) lib/task.ex:725: Task.await_many/5
#        (elixir 1.11.3) lib/task.ex:709: Task.await_many/2
通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 任务

练习已锁定

再解锁 1 个练习即可练习 任务