简介
当我第一次学习 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 则通过监督集成提供健壮的并发支持。每种工具对应不同用例,从简单计数器到复杂状态逻辑均有适用方案。