Elixir - 流程


在 Elixir 中,所有代码都在进程内运行。进程彼此隔离,彼此并发运行并通过消息传递进行通信。Elixir 的进程不应与操作系统进程混淆。Elixir 中的进程在内存和 CPU 方面非常轻量(与许多其他编程语言中的线程不同)。因此,数万甚至数十万个进程同时运行的情况并不少见。

在本章中,我们将了解生成新进程以及在不同进程之间发送和接收消息的基本构造。

生成函数

创建新进程的最简单方法是使用spawn函数。生成接受将在新进程运行的函数。例如 -

pid = spawn(fn -> 2 * 2 end)
Process.alive?(pid)

当上面的程序运行时,它会产生以下结果 -

false

spawn函数的返回值是一个PID。这是进程的唯一标识符,因此如果您运行 PID 上方的代码,它将有所不同。正如您在本例中看到的,当我们检查进程是否还活着时,该进程就已死亡。这是因为进程一旦完成运行给定的函数就会退出。

正如已经提到的,所有 Elixir 代码都在进程内运行。如果运行 self 函数,您将看到当前会话的 PID -

pid = self
 
Process.alive?(pid)

当上面的程序运行时,它会产生以下结果 -

true

消息传递

我们可以使用send向进程发送消息,并使用receive接收消息。让我们将消息传递给当前进程并在同一进程上接收它。

send(self(), {:hello, "Hi people"})

receive do
   {:hello, msg} -> IO.puts(msg)
   {:another_case, msg} -> IO.puts("This one won't match!")
end

当上面的程序运行时,它会产生以下结果 -

Hi people

我们使用send函数向当前进程发送了一条消息,并将其传递给self的PID。然后我们使用接收函数处理传入的消息。

当消息发送到进程时,该消息将存储在进程邮箱中。接收块遍历当前进程邮箱,搜索与任何给定模式匹配的消息。接收块支持保护和许多子句,例如 case。

如果邮箱中没有消息与任何模式匹配,则当前进程将等待,直到匹配的消息到达。还可以指定超时。例如,

receive do
   {:hello, msg}  -> msg
after
   1_000 -> "nothing after 1s"
end

当上面的程序运行时,它会产生以下结果 -

nothing after 1s

注意- 当您已经期望邮件到达邮箱时,可以指定超时 0。

链接

Elixir 中最常见的生成形式实际上是通过spawn_link函数。在查看 spawn_link 的示例之前,让我们先了解进程失败时会发生什么。

spawn fn -> raise "oops" end

运行上述程序时,会产生以下错误 -

[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
   :erlang.apply/2

它记录了一个错误,但生成过程仍在运行。这是因为进程是隔离的。如果我们希望一个进程中的失败传播到另一个进程,我们需要将它们链接起来。这可以通过spawn_link函数来完成。让我们考虑一个例子来理解同样的内容 -

spawn_link fn -> raise "oops" end

运行上述程序时,会产生以下错误 -

** (EXIT from #PID<0.41.0>) an exception was raised:
   ** (RuntimeError) oops
      :erlang.apply/2

如果您在iex shell中运行此命令,则 shell 会处理此错误并且不会退出。但是,如果您首先创建脚本文件,然后使用elixir <file-name>.exs来运行,则父进程也会由于此故障而被关闭。

流程和链路在构建容错系统时发挥着重要作用。在 Elixir 应用程序中,我们经常将进程链接到监督程序,监督程序将检测进程何时终止并在其位置启动一个新进程。这是可能的,因为进程是隔离的并且默认情况下不共享任何内容。由于进程是隔离的,进程中的故障不可能导致另一个进程崩溃或破坏状态。而其他语言则要求我们捕获/处理异常;在 Elixir 中,我们实际上可以让进程失败,因为我们希望主管能够正确地重新启动我们的系统。

状态

如果您正在构建需要状态的应用程序,例如,为了保留应用程序配置,或者您需要解析文件并将其保存在内存中,那么您会将其存储在哪里?Elixir 的流程功能在执行此类操作时会派上用场。

我们可以编写无限循环、维护状态以及发送和接收消息的进程。例如,让我们编写一个模块来启动新进程,该进程作为名为kv.exs的文件中的键值存储。

defmodule KV do
   def start_link do
      Task.start_link(fn -> loop(%{}) end)
   end

   defp loop(map) do
      receive do
         {:get, key, caller} ->
         send caller, Map.get(map, key)
         loop(map)
         {:put, key, value} ->
         loop(Map.put(map, key, value))
      end
   end
end

请注意,start_link函数启动一个运行循环函数的新进程,从一个空映射开始。然后,循环函数等待消息并对每条消息执行适当的操作。对于:get消息,它会将消息发送回调用者并再次调用循环以等待新消息。而:put消息实际上使用新版本的映射调用循环,并存储给定的键和值。

现在让我们运行以下命令 -

iex kv.exs

现在您应该处于iex shell 中。要测试我们的模块,请尝试以下操作 -

{:ok, pid} = KV.start_link

# pid now has the pid of our new process that is being 
# used to get and store key value pairs 

# Send a KV pair :hello, "Hello" to the process
send pid, {:put, :hello, "Hello"}

# Ask for the key :hello
send pid, {:get, :hello, self()}

# Print all the received messages on the current process.
flush()

当上面的程序运行时,它会产生以下结果 -

"Hello"