前端开发入门到精通的在线学习网站

网站首页 > 资源文章 正文

Elixir实战:5 并发原语 (3)有状态服务器进程

qiguaw 2025-02-03 14:40:30 资源文章 16 ℃ 0 评论

5.3 有状态服务器进程

生成进程以执行一次性任务并不是并发的唯一用例。在 Elixir 中,创建可以处理以消息形式发送的各种请求的长时间运行的进程是很常见的。此外,这些进程可能会维护一些内部状态——一个可能随时间变化的任意数据片段。

我们称这种过程为有状态服务器进程,它们是 Elixir 和 Erlang 系统中的一个重要概念,因此我们将花一些时间来探讨它们。

5.3.1 服务器进程

服务器进程是一个非正式名称,指的是一个长时间(或永远)运行并能够处理各种请求(消息)的进程。要使一个进程永远运行,必须使用无尽的尾递归。你可能还记得在第三章中,尾调用会受到特殊处理。如果一个函数的最后一件事是调用另一个函数(或它自己),那么会发生简单的跳转,而不是栈推送。因此,一个总是调用自己的函数将永远运行,而不会导致栈溢出或消耗额外的内存。

这可以用来实现一个服务器进程。您需要运行一个无限循环,并在循环的每一步中等待消息。当收到消息时,您处理它,然后继续循环。让我们通过创建一个可以按需运行查询的服务器进程来尝试这个。

以下列表提供了一个长期运行的服务器进程的基本草图。

清单 5.1 长时间运行的服务器进程 (database_server.ex)

defmodule DatabaseServer do
  def start do
    spawn(&loop/0)    
  end
 
  defp loop do
    receive do        
      ...             
    end               
 
    loop()            
  end
 
  ...
end

并发启动循环

处理一条消息

保持循环运行

start/0 是客户端用于启动服务器进程的所谓接口函数。当调用 start/0 时,它会生成运行 loop/0 函数的进程。该函数驱动进程的无限循环。该函数等待消息,处理消息,然后调用自身,确保进程永不停止。

这样的实现使得这个过程成为一个服务器。该过程大多数时间处于空闲状态,等待消息(请求)的到来,而不是主动运行某些计算。值得注意的是,这个循环并不占用 CPU 资源。等待消息使得进程处于挂起状态,并不会浪费 CPU 周期。

注意,本模块中的函数在不同的进程中运行。函数 start/0 由客户端调用,并在客户端进程中运行。私有函数 loop/0 在服务器进程中运行。来自同一模块的不同函数在不同进程中运行是完全正常的——模块和进程之间没有特殊关系。模块只是函数的集合,这些函数可以在任何进程中被调用。

在实现服务器进程时,通常将其所有代码放在一个模块中是有意义的。该模块的功能一般分为两类:接口和实现。接口函数是公共的,并在调用进程中执行。它们隐藏了进程创建和通信协议的细节。实现函数通常是私有的,并在服务器进程中运行。

注意 与经典循环一样,您通常不需要自己编写递归循环。提供了一种称为 GenServer (通用服务器进程)的标准抽象,它简化了有状态服务器进程的开发。该抽象仍然依赖于递归,但这种递归是在 GenServer 中实现的。您将在第 6 章中学习有关此抽象的内容。

让我们看看 loop/0 函数的完整实现。

清单 5.2 数据库服务器循环 (database_server.ex)

defmodule DatabaseServer do
  ...
 
  defp loop do
    receive do                                          
      {:run_query, caller, query_def} ->
        query_result = run_query(query_def)             
        send(caller, {:query_result, query_result})     
    end
 
    loop()
  end
 
  defp run_query(query_def) do                          
    Process.sleep(2000)
    "#{query_def} result"
  end
 
  ...
end

等待消息

运行查询并将响应发送给调用者

查询执行

此代码揭示了调用进程与数据库服务器之间的通信协议。调用者以格式 {:run_query, caller, query_def} 发送消息。服务器进程通过执行查询并将查询结果发送回调用进程来处理此类消息。

通常,您希望将这些通信细节隐藏起来,以免客户依赖于了解必须发送或接收的消息的确切结构。为了隐藏这些细节,最好提供一个专用的接口函数。我们引入一个名为 run_async/2 的函数,客户将使用它向服务器请求操作——在这种情况下,是查询执行。这个函数使客户对消息传递细节毫不知情;他们只需调用 run_async/2 并获取结果。实现细节在以下列表中给出。

清单 5.3 run_async/2 的实现 (database_server.ex)

defmodule DatabaseServer do
  ...
 
  def run_async(server_pid, query_def) do
    send(server_pid, {:run_query, self(), query_def})
  end
 
  ...
end

run_async/2 函数接收数据库服务器的 PID 和您想要执行的查询。它将适当的消息发送到服务器,然后不再做其他事情。从客户端调用 run_async/2 会请求服务器进程运行查询,而调用者则继续其业务。

一旦查询执行,服务器会向调用进程发送消息。要获取此结果,您需要添加另一个接口函数: get_result/0 。

清单 5.4 get_result/0 的实现 (database_server.ex)

defmodule DatabaseServer do
  ...
 
  def get_result do
    receive do
      {:query_result, result} -> result
    after
      5000 -> {:error, :timeout}
    end
  end
 
  ...
end 

get_result/0 在客户端想要获取查询结果时被调用。在这里,您使用 receive 来获取消息。 after 子句确保在经过一段时间后您放弃(例如,如果在查询执行期间出现问题且响应从未返回)。

数据库服务器现在已完成。让我们看看如何使用它:

iex(1)> server_pid = DatabaseServer.start()
 
iex(2)> DatabaseServer.run_async(server_pid, "query 1")
iex(3)> DatabaseServer.get_result()
"query 1 result"
 
iex(4)> DatabaseServer.run_async(server_pid, "query 2")
iex(5)> DatabaseServer.get_result()
"query 2 result"

注意如何在同一进程中执行多个查询。首先,运行查询 1,然后运行查询 2。这证明在接收到消息后,服务器进程仍然在运行。

因为通信细节被封装在函数中,客户端并不知道这些细节。相反,它通过普通函数与进程进行通信。在这里,服务器 PID 扮演着重要角色。您可以通过调用 DatabaseServer.start/0 来获取 PID,然后使用它向服务器发出请求。

当然,请求是在服务器进程中异步处理的。在调用 DatabaseServer.run_async/2 之后,您可以在客户端 ( iex ) 进程中随意操作,并在需要时收集结果。

服务器进程是顺序的

重要的是要意识到服务器进程在内部是顺序的。它运行一个循环,一次处理一条消息。因此,如果您向单个服务器进程发出五个异步查询请求,它们将一个接一个地处理,最后一个查询的结果将在 10 秒后返回。

这是一件好事,因为它帮助你推理系统。服务器进程可以被视为一个同步点。如果多个操作需要同步发生,以串行方式进行,你可以引入一个单一的进程,并将所有请求转发到该进程,由它顺序处理请求。

当然,在这种情况下,顺序属性是一个问题。您希望并发运行多个查询,以尽快获得结果。您能对此做些什么?

假设查询可以独立运行,您可以启动一组服务器进程,然后对于每个查询,您可以以某种方式从池中选择一个进程并让该进程运行查询。如果池足够大,并且您在池中的每个工作者之间均匀分配工作,您将尽可能地并行化总工作量。

这是一个基本的草图,说明如何做到这一点。首先,创建一个数据库服务器进程池:

iex(1)> pool = Enum.map(1..100, fn _ -> DatabaseServer.start() end)

在这里,您创建 100 个数据库服务器进程并将它们的 PID 存储在一个列表中。您可能认为 100 个进程很多,但请记住,进程是轻量级的。它们占用少量内存(约 2 KB),并且创建速度非常快(几微秒)。此外,由于所有这些进程都在等待消息,因此它们实际上是空闲的,不会浪费 CPU 时间。

接下来,当您运行查询时,您需要决定哪个进程将执行该查询。最简单的方法是使用 :rand.uniform/1 函数,该函数接受一个正整数 n 并返回范围 1..n 内的随机数。利用这一点,以下表达式将五个查询分配到一组进程中:

iex(2)> Enum.each(
          1..5,
          fn query_def ->
            server_pid = Enum.at(pool, :rand.uniform(100) - 1)    
            DatabaseServer.run_async(server_pid, query_def)       
          end
        )

选择一个随机过程

在其上运行查询

请注意,这并不高效;您正在使用 Enum.at/2 来选择一个随机的 PID。因为您使用列表来保存进程,而随机查找是 O(n) 操作,选择一个随机工作者的性能并不好。如果您使用一个以进程索引为键,PID 为值的映射,效果会更好。还有几种替代方法,例如使用轮询方法。但现在,让我们坚持这个简单的实现。

一旦您将查询排队到工作线程,您需要收集响应。这现在很简单,如以下代码片段所示:

iex(3)> Enum.map(1..5, fn _ -> DatabaseServer.get_result() end)
["5 result", "3 result", "1 result", "4 result", "2 result"]

因此,您可以更快地获得所有结果,因为查询再次是并发执行的。

5.3.2 保持进程状态

服务器进程打开了保持某种进程特定状态的可能性。例如,当您与数据库交谈时,您需要维护一个连接句柄。

要在进程中保持状态,您可以通过附加参数扩展 loop 函数。以下是一个基本示例:

def start do
  spawn(fn ->
    initial_state = ...   
    loop(initial_state)   
  end)
end
 
defp loop(state) do
  ...
  loop(state)             
end

在进程创建期间初始化状态

以该状态进入循环

在循环期间保持状态

让我们使用这种技术来扩展数据库服务器与连接。在这个例子中,您将使用一个随机数作为连接句柄的模拟。首先,您需要在进程启动时初始化连接,如以下列表所示。

清单 5.5 初始化进程状态 (stateful_database_server.ex)

defmodule DatabaseServer do
  ...
 
  def start do
    spawn(fn ->
      connection = :rand.uniform(1000)
      loop(connection)
    end)
  end
 
  ...
end

在这里,您打开连接,然后将相应的句柄传递给 loop 函数。在实际应用中,您会使用数据库客户端库(例如,ODBC)来打开连接,而不是生成随机数。

接下来,您需要修改 loop 函数。

清单 5.6 在查询时使用连接 (stateful_database_server.ex)

defmodule DatabaseServer do
  ...
 
  defp loop(connection) do
    receive do
      {:run_query, from_pid, query_def} ->
        query_result = run_query(connection, query_def)   
        send(from_pid, {:query_result, query_result})
      end
 
    loop(connection)                                      
  end
 
  defp run_query(connection, query_def) do
    Process.sleep(2000)
    "Connection #{connection}: #{query_def} result"
  end
 
  ...
end

在运行查询时使用连接

保持连接在循环参数中

loop 函数将状态(连接)作为第一个参数。每次循环恢复时,函数将状态传递给自身,因此在下一步中可用。

此外,您必须扩展 run_query 函数以在查询数据库时使用连接。连接句柄(在这种情况下是一个数字)包含在查询结果中。

通过这个,你的有状态数据库服务器已经完成。请注意,你没有改变其公共函数的接口,因此使用方式与之前相同。让我们看看它是如何工作的:

iex(1)> server_pid = DatabaseServer.start()
 
iex(2)> DatabaseServer.run_async(server_pid, "query 1")
iex(3)> DatabaseServer.get_result()
"Connection 753: query 1 result"
 
iex(4)> DatabaseServer.run_async(server_pid, "query 2")
iex(5)> DatabaseServer.get_result()
"Connection 753: query 2 result"

不同查询的结果使用相同的连接句柄执行,该句柄在进程循环中内部保持,对其他进程完全不可见。

5.3.3 可变状态

到目前为止,您已经看到如何保持恒定的过程特定状态。使这个状态可变并不需要太多。基本思路如下:

defp loop(state) do
  new_state =            
    receive do
      msg1 ->
        ...
 
      msg2 ->
        ...
    end
 
  loop(new_state)        
end

计算基于消息的新状态

带有新状态的循环

这是 Elixir 中的一种标准、有状态的服务器技术。该进程在处理消息时确定新状态。然后,循环函数使用新状态调用自身,这实际上改变了状态。下一个接收到的消息在新状态上操作。

从外部来看,有状态的进程是可变的。通过向进程发送消息,调用者可以影响其状态以及该服务器处理的后续请求的结果。从这个意义上说,发送消息是一种可能具有副作用的操作。尽管如此,服务器仍然依赖于不可变的数据结构。状态可以是任何有效的 Elixir 变量,从简单的数字到复杂的数据抽象,例如 TodoList (这是你在第 4 章中构建的)。

让我们看看这个实际应用。你将从一个简单的例子开始:一个有状态的计算器进程,它将一个数字作为其状态。最初,进程的状态是 0,你可以通过发出请求来操控它,例如 add 、 sub 、 mul 和 div 。你还可以通过 value 请求来检索进程状态。

这是您使用服务器的方法:

iex(1)> calculator_pid = Calculator.start()      
 
iex(2)> Calculator.value(calculator_pid)         
0                                                
 
iex(3)> Calculator.add(calculator_pid, 10)       
iex(4)> Calculator.sub(calculator_pid, 5)        
iex(5)> Calculator.mul(calculator_pid, 3)        
iex(6)> Calculator.div(calculator_pid, 5)        
 
iex(7)> Calculator.value(calculator_pid)         
3.0

开始过程

验证初始值

问题请求

验证值

在这段代码中,您启动了该过程并检查其初始状态。然后,您发出了一些修改请求并验证操作的结果 (((0 + 10) - 5) * 3) / 5 ,结果是 3.0 。

现在,是时候实现这一点了。首先,让我们看看服务器的内部循环。

清单 5.7 并发有状态计算器 (calculator.ex)

defmodule Calculator do
  ...
 
  defp loop(current_value) do
    new_value =
      receive do
        {:value, caller} ->                                         
          send(caller, {:response, current_value})                  
          current_value                                             
 
        {:add, value} -> current_value + value                      
        {:sub, value} -> current_value - value                      
        {:mul, value} -> current_value * value                      
        {:div, value} -> current_value / value                      
 
        invalid_request ->                                          
          IO.puts("invalid request #{inspect invalid_request}")     
          current_value                                             
      end
 
    loop(new_value)
  end
 
  ...
end

获取请求

算术运算请求

不支持的请求

循环处理各种消息。 :value 消息用于检索服务器的状态。因为您需要将响应发送回去,调用者必须在消息中包含其 PID。请注意,此块的最后一个表达式返回 current_value 。这是必要的,因为 receive 的结果存储在 new_value 中,然后用于服务器的新状态。通过返回 current_value ,您指定 :value 请求不会改变进程状态。

算术运算根据当前值和消息中接收到的参数计算新状态。与 :value 消息处理程序不同,算术运算处理程序不会向调用者发送响应。这使得可以异步运行这些操作,正如您在实现接口函数时很快会看到的那样。

最后的 receive 子句匹配所有其他消息。这些是您不支持的消息,因此您将它们记录到屏幕上并返回 current_value ,保持状态不变。

接下来,您需要实现客户端将使用的接口函数。这将在下一个列表中完成。

清单 5.8 Calculator 接口函数 (calculator.ex)

defmodule Calculator do
  def start do                                                      
    spawn(fn -> loop(0) end)                                        
  end                                                               
 
  def value(server_pid) do                                          
    send(server_pid, {:value, self()})                              
                                                                    
    receive do                                                      
      {:response, value} ->                                         
        value                                                       
      end                                                           
    end                                                             
 
  def add(server_pid, value), do: send(server_pid, {:add, value})   
  def sub(server_pid, value), do: send(server_pid, {:sub, value})   
  def mul(server_pid, value), do: send(server_pid, {:mul, value})   
  def div(server_pid, value), do: send(server_pid, {:div, value})   
 
  ...
end

启动服务器并初始化状态

价值请求

算术运算

接口功能遵循 loop/1 函数中指定的协议。 :value 请求是第 5.2.2 节中提到的同步消息传递的一个示例。调用者发送消息,然后等待响应。调用者在响应返回之前被阻塞,这使得请求处理是同步的。

算术操作是异步运行的。没有响应消息,因此调用者不需要等待任何东西。因此,调用者可以发出多个这样的请求,并在操作在服务器进程中并发运行时继续自己的工作。请记住,服务器按接收顺序处理消息,因此请求会按正确的顺序处理。

为什么要使算术操作异步?因为你不关心它们何时执行。在你请求服务器的状态(通过 value/1 函数)之前,你不希望客户端被阻塞。这使得客户端更高效,因为在服务器进行计算时它不会被阻塞。

重构循环

随着您向服务器引入多个请求, loop 函数变得更加复杂。如果您必须处理许多请求,它将变得臃肿,变成一个巨大的 switch/case -like 表达式。

您可以通过依赖模式匹配并将消息处理移动到一个单独的多条件函数来重构此代码。这使得 loop 函数的代码保持非常简单:

defp loop(current_value) do
  new_value =
    receive do
      message -> process_message(current_value, message)
    end
 
  loop(new_value)
end

查看这段代码,您可以看到服务器的一般工作流程。首先接收一条消息,然后进行处理。消息处理通常是根据当前状态和接收到的消息计算新状态。最后,您使用这个新状态进行循环,有效地将其设置为旧状态的替代。

process_message/2 是一个简单的多子句函数,它接收当前状态和消息。它的任务是执行特定于消息的代码并返回新状态:

defp process_message(current_value, {:value, caller}) do
  send(caller, {:response, current_value})
  current_value
end
 
defp process_message(current_value, {:add, value}) do
  current_value + value
end
 
...

这段代码是服务器进程循环的简单重组。它允许您保持循环代码的紧凑性,并将消息处理细节移动到一个单独的多条款函数中,每个条款处理特定的消息。

5.3.4 复杂状态

状态通常比一个简单的数字复杂得多。然而,技术始终保持不变:您使用私有 loop 函数保持可变状态。随着状态变得越来越复杂,服务器进程的代码可能会变得越来越复杂。值得将状态操作提取到一个单独的模块中,并让服务器进程专注于传递消息和保持状态。

让我们看看在第 4 章中开发的 TodoList 抽象的这个技术。首先,让我们回顾一下该结构的基本用法:

iex(1)> todo_list =
          TodoList.new() |>
          TodoList.add_entry(%{date: ~D[2023-12-19], title: "Dentist"}) |>
          TodoList.add_entry(%{date: ~D[2023-12-20], title: "Shopping"}) |>
          TodoList.add_entry(%{date: ~D[2023-12-19], title: "Movies"})
 
iex(2)> TodoList.entries(todo_list, ~D[2023-12-19])
[
  %{date: ~D[2023-12-19], id: 1, title: "Dentist"},
  %{date: ~D[2023-12-19], id: 3, title: "Movies"}
]

正如您所记得的, TodoList 是一种纯函数抽象。为了保持结构的活性,您必须不断保留对在该结构上执行的最后一个操作的结果。

在这个例子中,您将构建一个 TodoServer 模块,该模块将此抽象保持在私有状态中。让我们看看服务器是如何使用的:

iex(1)> todo_server = TodoServer.start()
 
iex(2)> TodoServer.add_entry(
          todo_server,
          %{date: ~D[2023-12-19], title: "Dentist"}
        )
 
iex(3)> TodoServer.add_entry(
          todo_server,
          %{date: ~D[2023-12-20], title: "Shopping"}
        )
 
iex(4)> TodoServer.add_entry(
          todo_server,
          %{date: ~D[2023-12-19], title: "Movies"}
        )
 
iex(5)> TodoServer.entries(todo_server, ~D[2023-12-19])
[
  %{date: ~D[2023-12-19], id: 3, title: "Movies"},
  %{date: ~D[2023-12-19], id: 1, title: "Dentist"}
]

您启动服务器,然后通过 TodoServer API 与其交互。与纯函数式方法相比,您不需要将修改的结果作为参数传递给下一个操作。相反,您不断使用相同的 todo_server 变量来处理待办事项列表。

让我们开始实现这个服务器。首先,您需要将所有模块放在一个文件中。

清单 5.9 TodoServer 模块 (todo_server.ex)

defmodule TodoServer do
  ...
end
 
defmodule TodoList do
  ...
end

将两个模块放在同一个文件中可以确保在启动 iex shell 时加载文件时可以使用所有内容。在更复杂的系统中,您会使用一个合适的 Mix 项目,正如第 7 章将要解释的那样,但现在,这已经足够。

TodoList 的实现与第 4 章相同。它具有在服务器进程中使用所需的所有功能。

现在,设置待办服务器进程的基本结构。

清单 5.10 TodoServer 基本结构 (todo_server.ex)

defmodule TodoServer do
  def start do
    spawn(fn -> loop(TodoList.new()) end)    
  end
 
  defp loop(todo_list) do
    new_todo_list =
      receive do
        message -> process_message(todo_list, message)
      end
 
    loop(new_todo_list)
  end
 
  ...
end

使用待办事项列表作为初始状态

这里没有什么新鲜事。你使用一个新的 TodoList 抽象实例作为初始状态来开始循环。在循环中,你接收消息并通过调用 process_message/2 函数将其应用于状态,该函数返回新状态。最后,你使用新状态进行循环。

对于您想要支持的每个请求,您必须在 process_message/2 函数中添加一个专用条款。此外,必须引入一个相应的接口函数。您将首先支持 add_entry 请求。

清单 5.11 add_entry 请求 (todo_server.ex)

defmodule TodoServer do
  ...
 
  def add_entry(todo_server, new_entry) do                       
    send(todo_server, {:add_entry, new_entry})                   
  end                                                            
 
  ...
 
  defp process_message(todo_list, {:add_entry, new_entry}) do    
    TodoList.add_entry(todo_list, new_entry)                     
  end                                                            
 
  ...
end

接口功能

消息处理程序条款

接口函数将新条目数据发送到服务器。此消息将在 process_message/2 的相应条款中处理。在这里,您委托给 TodoList.add_entry/2 函数并返回修改后的 TodoList 实例。此返回的实例用作新的服务器状态。

使用类似的方法,您可以实现 entries 请求,记住您需要等待响应消息。代码在下一个列表中显示。

清单 5.12 entries 请求 (todo_server.ex)

defmodule TodoServer do
  ...
 
  def entries(todo_server, date) do
    send(todo_server, {:entries, self(), date})
 
    receive do
      {:todo_entries, entries} -> entries
    after
      5000 -> {:error, :timeout}
    end
  end
 
  ...
 
  defp process_message(todo_list, {:entries, caller, date}) do
    send(caller, {:todo_entries, TodoList.entries(todo_list, date)})   
    todo_list                                                          
  end
 
  ...
end

将响应发送给呼叫者

状态保持不变。

这是您之前看到的技术的综合。您发送一条消息并等待响应。在相应的 process_message/2 子句中,您委托给 TodoList ,然后您发送响应并返回未更改的待办事项列表。这是必要的,因为 loop/2 将 process_message/2 的结果作为新状态。

以类似的方式,您可以添加对其他待办事项请求的支持,例如 update_entry 和 delete_entry 。这些请求的实现留给您作为练习。

并发与功能方法

一个维护可变状态的过程可以被视为一种可变数据结构。但你不应该滥用过程来避免使用转换不可变数据的函数式方法。

数据应使用纯函数抽象进行建模,就像您在 TodoList 中所做的那样。纯函数数据结构提供了许多好处,例如完整性、原子性、可重用性和可测试性。

一个有状态的过程充当这种数据结构的容器。该过程保持状态的活跃,并允许系统中的其他过程通过暴露的 API 与该数据进行交互。

通过这种责任分离,构建一个高度并发的系统变得简单。例如,如果您正在实现一个管理多个待办事项列表的网络服务器,您可以为每个待办事项列表运行一个服务器进程。在处理 HTTP 请求时,您可以找到相应的待办事项服务器,并让它执行请求的操作。每个待办事项的操作都是并发运行的,从而使您的服务器可扩展且性能更高。此外,由于每个待办事项列表在专用进程中管理,因此没有同步问题。请记住,单个进程始终是顺序的,因此多个竞争请求操作同一个待办事项列表时,会在相应的进程中被序列化并顺序处理。如果这看起来模糊,不用担心——您将在第 7 章中看到它的实际应用。

5.3.5 注册的进程

为了使一个进程与其他进程协作,它必须知道它们的位置。在 BEAM 中,进程通过其对应的 PID 进行识别。要使进程 A 向进程 B 发送消息,您必须将进程 B 的 PID 传递给进程 A。

有时候,保持和传递 PID 可能会很麻烦。如果你知道某种类型的服务器将始终只有一个实例,你可以给该进程一个本地名称,并使用该名称向进程发送消息。这个名称被称为本地名称,因为它仅在当前运行的 BEAM 实例中有意义。当你开始将多个 BEAM 实例连接到分布式系统时,这个区别变得很重要,正如你将在第 12 章中看到的。

注册一个进程可以通过 Process.register(pid, name) 完成,其中名称必须是一个原子。这里有一个快速的示例:

iex(1)> Process.register(self(), :some_name)    
 
iex(2)> send(:some_name, :msg)                  
 
iex(3)> receive do                              
          msg -> IO.puts("received #{msg}")
        end
 
received msg

注册一个进程

通过符号名称发送消息

验证消息是否已接收

以下约束适用于注册名称:

  • 名称只能是一个原子。
  • 一个进程只能有一个名称。
  • 两个进程不能有相同的名称。

如果这些约束未被满足,将会引发错误。

为了练习,尝试将待办事项服务器更改为以注册进程的方式运行。服务器的接口将因此简化,因为您不需要保留和传递服务器的 PID。

这是一个如何使用此类服务器的示例:

iex(1)> TodoServer.start()
 
iex(2)> TodoServer.add_entry(%{date: ~D[2023-12-19], title: "Dentist"})
iex(3)> TodoServer.add_entry(%{date: ~D[2023-12-20], title: "Shopping"})
iex(4)> TodoServer.add_entry(%{date: ~D[2023-12-19], title: "Movies"})
 
iex(5)> TodoServer.entries(~D[2023-12-19])
[%{date: ~D[2023-12-19], id: 3, title: "Movies"},
 %{date: ~D[2023-12-19], id: 1, title: "Dentist"}]

要使其工作,您必须在一个名称下注册一个服务器进程(例如, :todo_ server )。然后,您需要更改所有接口函数,以在向进程发送消息时使用注册的名称。如果您遇到困难,解决方案在 registered_todo_server.ex 文件中提供。

使用注册的服务器要简单得多,因为您不必存储服务器的 PID 并将其传递给接口函数。相反,接口函数内部使用注册名称向进程发送消息。

本地注册在流程发现中发挥着重要作用。注册名称提供了一种与流程进行通信的方式,而无需知道其 PID。当您开始处理重启流程(如第 8 章和第 9 章所示)和分布式系统(在第 12 章中讨论)时,这一点变得越来越重要。

这结束了我们对有状态进程的初步探索。它们在基于 Elixir 的系统中扮演着重要角色,您将在整本书中继续使用它们。接下来,我们将查看 BEAM 进程的一些重要运行时属性。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表