Elixir 中的状态管理:进程、Agent 与 GenServer 实践

发布于:2025-07-02 ⋅ 阅读:(23) ⋅ 点赞:(0)

简介  

当我第一次学习 Elixir 时,我曾困惑于如何管理状态。与通过可变全局变量实现状态管理的命令式语言不同,Elixir 的不可变数据模型和基于 BEAM 虚拟机的并发设计要求采用不同的方法。本文将探讨 Elixir 中状态的处理方式。

上下文:BEAM 虚拟机与并发  

Elixir 运行在专为高并发和容错设计的 BEAM 虚拟机上。受 Actor 模型启发,BEAM 将进程视为通过消息传递通信的轻量级实体。由于数据不可变,状态变更通过创建新值而非修改现有值实现。这确保了线程安全并简化了并发编程。

递归循环  

维护状态的最简单方法是实现递归循环。示例如下:

defmodule StatefulMap do
  def start do
    spawn(fn -> loop(%{}) end)
  end

  def loop(current) do
    new =
      receive do
        message -> process(current, message)
      end

    loop(new)
  end

  def put(pid, key, value) do
    send(pid, {:put, key, value})
  end

  def get(pid, key) do
    send(pid, {:get, key, self})

    receive do
      {:response, value} -> value
    end
  end

  defp process(current, {:put, key, value}) do
    Map.put(current, key, value)
  end

  defp process(current, {:get, key, caller}) do
    send(caller, {:response, Map.get(current, key)})
    current
  end
end

使用示例:

pid = StatefulMap.start() # PID<0.63.0>
StatefulMap.put(pid, :hello, :world)
StatefulMap.get(pid, :hello) # :world

Agent  

另一种选择是使用 Agent 模块,它允许在不同进程或同一进程的不同时间点共享状态。

示例实现:

defmodule Counter do
  use Agent

  def start_link(initial_value) do
    Agent.start_link(fn -> initial_value end, name: __MODULE__)
  end

  def value do
    Agent.get(__MODULE__, & &1)
  end

  def increment do
    Agent.update(__MODULE__, &(&1 + 1))
  end
end

使用示例:

Counter.start_link(0)
#=> {:ok, #PID<0.123.0>}

Counter.value()
#=> 0

Counter.increment()
#=> :ok

Counter.increment()
#=> :ok

Counter.value()
#=> 2

推荐通过监督者启动:

children = [
  {Counter, 0}
]

Supervisor.start_link(children, strategy: :one_for_all)

GenServer  

最经典的选择是 GenServer 行为模式(类似 .NET/Java 中的接口),它支持通过同步和异步请求管理状态。

关键回调函数:  

  • init/1 → 进程启动时调用  
  • handle_call/2 → 同步请求(如需响应)  
  • handle_cast/3 → 异步请求(如“发送即忘”)

示例代码:

defmodule Stack do
  use GenServer

  # 回调函数

  @impl true
  def init(elements) do
    initial_state = String.split(elements, ",", trim: true)
    {:ok, initial_state}
  end

  @impl true
  def handle_call(:pop, _from, state) do
    [to_caller | new_state] = state
    {:reply, to_caller, new_state}
  end

  @impl true
  def handle_cast({:push, element}, state) do
    new_state = [element | state]
    {:noreply, new_state}
  end
end

使用示例:

# 启动服务器
{:ok, pid} = GenServer.start_link(Stack, "hello,world")

# 客户端代码
GenServer.call(pid, :pop)
#=> "hello"

GenServer.cast(pid, {:push, "elixir"})
#=> :ok

GenServer.call(pid, :pop)
#=> "elixir"

结论  

Elixir 的状态管理依赖进程与不可变性。递归循环提供基础控制,Agent 简化共享状态管理,GenServer 则通过监督集成提供健壮的并发支持。每种工具对应不同用例,从简单计数器到复杂状态逻辑均有适用方案。

参考资料  

Working with State and Elixir Processes
GenServer  
Agent