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

消息传递

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

当消息发送到进程时,消息将存储在进程邮箱中。receive 块遍历当前进程邮箱,搜索与任何给定模式匹配的消息。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

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

** (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函数启动一个运行loop函数的新进程,从空映射开始。然后,loop函数等待消息并对每条消息执行相应的操作。对于:get消息,它将消息发送回调用方并再次调用 loop,以等待新消息。而:put消息实际上使用新的映射版本调用loop,其中存储了给定的键值。

现在让我们运行以下代码:

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"
广告