Elixir - 进程

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

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

spawn 函数

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

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

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

false

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

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

pid = self
 
Process.alive?(pid)

运行上述程序时,会产生以下结果 −

true

Message Passing

我们可以使用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。 然后我们使用 receive 函数处理传入消息。

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

When the above program is run, it produces the following error −

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

如果您在 iex shell 中运行此命令,则 shell 会处理此错误并且不会退出。 但如果你先制作一个脚本文件,然后使用 elixir .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 函数启动一个运行loop 函数的新进程,从空映射开始。 然后,loop 函数等待消息并对每条消息执行适当的操作。 对于 :get 消息,它会将消息发送回调用者并再次调用循环,以等待新消息。 而 :put 消息实际上使用新版本的映射调用 loop,并存储给定的键和值。

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

iex kv.exs

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

{:ok, pid} = KV.start_link

# pid 现在拥有我们正在创建的新进程的 pid
# 用于获取和存储键值对

# 向进程发送一个KV对:hello, "Hello"
send pid, {:put, :hello, "Hello"}

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

# 打印当前进程接收到的所有消息。
flush()

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

"Hello"