Elixir GenServer

GenServer 是一个用来实现客户端-服务器模式中服务器的行为模块。

GenServer 是一个普通的 Elixir 进程,同其他 Elixir 进程一样,它可以用来保存状态、异步执行代码等。使用这个模块来实现通用服务器进程(GenServer)的优势在于,它有一套标准的接口函数,并提供用于跟踪和错误报告的功能。同时它也可以用到一个监督树中。

request
request
request
reply
reply
reply
GenServer
Client #1
Client #2
Client #3

示例

GenServer 行为抽象了常见的客户端-服务器模式。开发人员只需要实现他们感兴趣的回调和功能。

让我们从一个例子开始,然后探索可用的回调。假设我们想用 GenServer 实现一个栈服务,可以压入和弹出元素。我们将通过实现三个回调来定制一个通用 GenServer。

init/1 将我们的初始参数转换为 GenServer 的初始状态。 handle_call/3 在服务器收到同步的 pop 消息时触发,从栈中弹出一个元素并返回给用户。 handle_cast/2 在服务器收到异步的 push 消息时触发,将一个元素压入栈中:

defmodule Stack do
  use GenServer

  # Callbacks

  @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

我们将进程的启动、消息传递和消息循环机制留给了 GenServer 行为,我们只用关注栈的具体实现。现在我们可以创建一个进程,并使用 GenServer API 发送消息来与服务交互了:

# Start the server
{:ok, pid} = GenServer.start_link(Stack, "hello,world")

# This is the client
GenServer.call(pid, :pop)
#=> "hello"

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

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

我们通过调用 start_link/2 来启动我们的 Stack ,它需要服务的实现模块和初始参数。GenServer 行为会调用 init/1 回调来生成 GenServer 的初始状态。从此刻开始,GenServer 接管一切,因此我们可以通过向它发送两种类型的消息与它交互。call 消息期望从服务器获得回复(因此是同步的),而 cast 消息则不需要回复。

GenServer.call/3 产生的消息必须由 GenServer 的 handle_call/3 回调处理。而 cast/2 的消息必须由 handle_cast/2 处理。 GenServer 支持 8 个回调,但只有 init/1 是必需的。

当你使用 use GenServer 时, GenServer 模块会设置 @behaviour GenServer 并定义一个 child_spec/1 函数,因此你的模块可以做为监督树的子进程。

客户端/服务端 API

尽管在上述示例中我们直接使用了 GenServer.start_link/3 和相关函数来启动服务器并与之通信,但大多数时候我们不直接调用 GenServer 函数。相反,我们将调用封装到新的函数中,做为服务器的公共 API。这些封装被称为客户端 API

以下是我们栈模块的升级版:

defmodule Stack do
  use GenServer

  # Client

  def start_link(default) when is_binary(default) do
    GenServer.start_link(__MODULE__, default)
  end

  def push(pid, element) do
    GenServer.cast(pid, {:push, element})
  end

  def pop(pid) do
    GenServer.call(pid, :pop)
  end

  # Server (callbacks)

  @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

实际上,将服务器和客户端的函数放在同一个模块中是很常见的。如果服务器或客户端变得越来越复杂,你可能希望将它们放到不同的模块中。

下图总结了客户端和服务器之间的交互。客户端和服务器都是进程,它们通过消息(实线)进行通信。服务器与实现模块的交互发生在 GenServer 进程调用你的代码时(虚线):

Client (Process) Server (Process) Module (Code) Typically started by a supervisor GenServer.start_link(module, arg, options) init(arg) {:ok, state} | :ignore | {:error, reason} {:ok, pid} | :ignore | {:error, reason} call is synchronous GenServer.call(pid, message) handle_call(message, from, state) {:reply, reply, state} | {:stop, reason, reply, state} reply cast is asynchronous GenServer.cast(pid, message) handle_cast(message, state) {:noreply, state} | {:stop, reason, state} send is asynchronous Kernel.send(pid, message) handle_info(message, state) {:noreply, state} | {:stop, reason, state} Client (Process) Server (Process) Module (Code)

如何监督

一个 GenServer 通常在监督树下启动。当我们调用 use GenServer 时,它会自动定义了一个 child_spec/1 函数,让我们可以直接在监督者下启动 Stack 。通过以下代码我们可以在监督者下启动一个初始为 ["hello", "world"] 的栈:

children = [
  {Stack, "hello,world"}
]

Supervisor.start_link(children, strategy: :one_for_all)

use GenServer 还接受一个选项列表来配置子进程描述,控制它在监督者下的运行方式。生成的 child_spec/1 可以使用以下选项自定义:

  • :id - 子进程描述的唯一标识,默认是当前模块名。
  • :restart - 子进程是否应该被重启,默认为 :permanent
  • :shutdown - 如何关闭子进程,立即关闭或给一段时间关闭。

例如:

use GenServer, restart: :transient, shutdown: 10_000

详细信息还请参见 Supervisor 模块中的“子进程描述”部分。紧接在 use GenServer 之前的 @doc 注释会添加到生成的 child_spec/1 函数上。

当停止 GenServer 时,例如从回调返回 {:stop, reason, new_state} 元组,退出原因会被监督者用来确定是否需要重启 GenServer。更多信息请参见 Supervisor 模块中的“退出原因和重启”部分。

命名注册

start_link/3start/3 都支持 GenServer 在启动时通过 :name 选项注册一个名称。注册的名称也会在进程终止时自动清理。支持的值包括:

  • 一个原子 - 使用 Process.register/2 ,GenServer 以此为名称注册到本地(当前节点)。
  • {:global, term} - 使用 :global 模块的函数将 GenServer 注册到全局,term 是服务名称。
  • {:via, module, term} - 使用自定义的机制和名称注册服务。 :via 选项需要一个导出了 register_name/2unregister_name/1whereis_name/1send/2 函数的模块。 :global 就是一个这样的模块,它使用这些函数来管理进程名称和PID,使它们对 Elixir 节点网络全局可用。Elixir 还提供了一个本地的、去中心化的、可扩展的注册表叫做 Registry ,用于本地存储动态生成的名称。

进程名称和 PID 之间的关系就像域名和 IP 地址一样。

例如,我们可以像下面这样本地启动并注册 Stack 服务:

# Start the server and register it locally with name MyStack
{:ok, _} = GenServer.start_link(Stack, "hello", name: MyStack)

# Now messages can be sent directly to MyStack
GenServer.call(MyStack, :pop)
#=> "hello"

一旦服务启动, GenServer 模块的其他函数(call/3cast/2 等)除了可以通过 PID 来调用,也可以通过 {:global, ...}{:via, ...} 元组调用。总的来说支持以下格式:

  • PID
  • 原子,如果服务是本地注册的话
  • {atom, node} ,服务在另一个节点本地注册
  • {:global, term} ,服务是全局注册的话
  • {:via, module, name} ,服务通过其他模块自定义注册

如果想要在本地注册动态名称,请不要使用原子,因为原子不会被垃圾回收,因此动态生成的原子也不会被垃圾回收。对于这种情况,可以使用 Registry 模块做为自己的本地注册服务。

接收“常规”消息

GenServer 的目标是为开发者抽象“接收”循环,自动处理系统消息,支持代码变更、同步调用等。因此,永远不要在 GenServer 回调中调用你自己的“接收”代码,这样做会导致 GenServer 行为异常。

除了 call/3cast/2 提供的同步和异步通信外,通过 send/2Process.send_after/4 等发送的“常规”消息可以在 handle_info/2 回调中处理。

handle_info/2 可用于许多情况,例如处理由 Process.monitor/1 发送的监控 DOWN 消息。 handle_info/2 的另一个用例是借助 Process.send_after/4 执行周期性工作:

defmodule MyApp.Periodically do
  use GenServer

  def start_link(_) do
    GenServer.start_link(__MODULE__, %{})
  end

  @impl true
  def init(state) do
    # Schedule work to be performed on start
    schedule_work()

    {:ok, state}
  end

  @impl true
  def handle_info(:work, state) do
    # Do the desired work here
    # ...

    # Reschedule once more
    schedule_work()

    {:noreply, state}
  end

  defp schedule_work do
    # We schedule the work to happen in 2 hours (written in milliseconds).
    # Alternatively, one might write :timer.hours(2)
    Process.send_after(self(), :work, 2 * 60 * 60 * 1000)
  end
end

超时

init/1 或任何 handle_* 回调的返回值都可以包含一个单位为毫秒的超时时间;如果没有,则为 :infinity 。超时可以用来检测没有消息的空闲期。

timeout() 的用法如下:

  • 如果在超时到达时进程有正在等待处理的消息,超时将被忽略,等待的消息会像往常一样被处理。这意味着即使是 0 毫秒的超时也不能保证被执行(如果你想立即无条件地执行某个动作,请改用 :continue 指令)。
  • 如果在超时到达之前有消息到达,超时将被清除,该消息会像往常一样被处理。
  • 否则,当超时时间过去后没有消息到达, handle_info/2 将被调用,并以 :timeout 作为第一个参数。

何时(不)使用 GenServer

到目前为止,我们已经了解到 GenServer 可以做为被监督的进程用来处理同步和异步调用。还可以处理系统消息,如周期性消息和监控事件。GenServer 进程也可以被命名。

一个 GenServer,或者更通俗的说,一个进程,是用来建模系统的运行时特性的,绝不能被用来组织代码。

在 Elixir 中,代码通过模块和函数来组织,进程不是必须的。例如,想象你正在实现一个计算器,你决定将所有的计算操作放在一个 GenServer 后面:

def add(a, b) do
  GenServer.call(__MODULE__, {:add, a, b})
end

def subtract(a, b) do
  GenServer.call(__MODULE__, {:subtract, a, b})
end

def handle_call({:add, a, b}, _from, state) do
  {:reply, a + b, state}
end

def handle_call({:subtract, a, b}, _from, state) do
  {:reply, a - b, state}
end

这就是一个反模式,不仅是因为它使计算器逻辑变得复杂,而且因为你将计算逻辑放在了一个单一的进程里面,这个进程可能会成为你系统的瓶颈,特别是随着调用数量增加时。直接定义为函数就行了:

def add(a, b) do
  a + b
end

def subtract(a, b) do
  a - b
end

能不使用进程就不要使用进程。只有在需要建模运行时属性时才会使用进程,如可变状态、并发和失败,而不是为了组织代码。

使用 :sys 模块进行调试

GenServers 作为一种特殊进程,可以使用 :sys 模块进行调试。通过各种钩子,这个模块允许开发者查看进程的状态,并跟踪执行期间发生的系统事件,如收到的消息、发送的回复以及状态变化。

以下是 :sys 模块用于调试的基本函数:

  • :sys.get_state/2 - 检索进程的状态。在 GenServer 中下,就是回调模块状态,即传递给回调函数的最后一个参数。
  • :sys.get_status/2 - 检索进程的状态。这个状态包括进程字典,如果进程正在运行或被挂起,父 PID,调试器状态,以及行为模块的状态,其中包括回调模块状态(由 :sys.get_state/2 返回)。可以通过实现可选的 GenServer.format_status/1 回调来改变这个状态的格式。
  • :sys.trace/3 - 将所有系统事件打印到 :stdio
  • :sys.statistics/3 - 管理(打开/关闭)进程统计信息的收集。
  • :sys.no_debug/2 - 关闭给定进程的所有调试句柄。结束后关闭调试是非常重要的。过多的调试句柄或者本应关闭但没有关闭的,可能会严重影响系统的性能。
  • :sys.suspend/2 - 挂起一个进程,让它只回复系统消息,不回复其他消息。挂起的进程可以通过 :sys.resume/2 重新激活。

让我们看看如何使用这些函数来调试我们之前定义的栈服务器。

{:ok, pid} = Stack.start_link([])
:sys.statistics(pid, true) # turn on collecting process statistics
:sys.trace(pid, true) # turn on event printing
Stack.push(pid, 1)
*DBG* <0.122.0> got cast {push,1}
*DBG* <0.122.0> new state [1]
:ok

:sys.get_state(pid)
[1]

Stack.pop(pid)
*DBG* <0.122.0> got call pop from <0.80.0>
*DBG* <0.122.0> sent 1 to <0.80.0>, new state []
1

:sys.statistics(pid, :get)
{:ok,
 [
   start_time: {{2016, 7, 16}, {12, 29, 41}},
   current_time: {{2016, 7, 16}, {12, 29, 50}},
   reductions: 117,
   messages_in: 2,
   messages_out: 0
 ]}

:sys.no_debug(pid) # turn off all debug handlers
:ok

:sys.get_status(pid)
{:status, #PID<0.122.0>, {:module, :gen_server},
 [
   [
     "$initial_call": {Stack, :init, 1},            # process dictionary
     "$ancestors": [#PID<0.80.0>, #PID<0.51.0>]
   ],
   :running,                                        # :running | :suspended
   #PID<0.80.0>,                                    # parent
   [],                                              # debugger state
   [
     header: 'Status for generic server <0.122.0>', # module status
     data: [
       {'Status', :running},
       {'Parent', #PID<0.80.0>},
       {'Logged events', []}
     ],
     data: [{'State', [1]}]
   ]
 ]}
标题“51单片机通过MPU6050-DMP获取姿态角例程”解析 “51单片机通过MPU6050-DMP获取姿态角例程”是一个基于51系列单片机(一种常见的8位微控制器)的程序示例,用于读取MPU6050传感器的数据,并通过其内置的数字运动处理器(DMP)计算设备的姿态角(如倾斜角度、旋转角度等)。MPU6050是一款集成三轴加速度计和三轴陀螺仪的六自由度传感器,广泛应用于运动控制和姿态检测领域。该例程利用MPU6050的DMP功能,由DMP处理复杂的运动学算法,例如姿态融合,将加速度计和陀螺仪的数据进行整合,从而提供稳定且实时的姿态估计,减轻主控MCU的计算负担。最终,姿态角数据通过LCD1602显示屏以字符形式可视化展示,为用户提供直观的反馈。 从标签“51单片机 6050”可知,该项目主要涉及51单片机和MPU6050传感器这两个关键硬件组件。51单片机基于8051内核,因编程简单、成本低而被广泛应用;MPU6050作为惯性测量单元(IMU),可测量设备的线性和角速度。文件名“51-DMP-NET”可能表示这是一个与51单片机及DMP相关的网络资源或代码库,其中可能包含C语言等适合51单片机的编程语言的源代码、配置文件、用户手册、示例程序,以及可能的调试工具或IDE项目文件。 实现该项目需以下步骤:首先是硬件连接,将51单片机与MPU6050通过I2C接口正确连接,同时将LCD1602连接到51单片机的串行数据线和控制线上;接着是初始化设置,配置51单片机的I/O端口,初始化I2C通信协议,设置MPU6050的工作模式和数据输出速率;然后是DMP配置,启用MPU6050的DMP功能,加载预编译的DMP固件,并设置DMP输出数据的中断;之后是数据读取,通过中断服务程序从DMP接收姿态角数据,数据通常以四元数或欧拉角形式呈现;再接着是数据显示,将姿态角数据转换为可读的度数格
MathorCup高校数学建模挑战赛是一项旨在提升学生数学应用、创新和团队协作能力的年度竞赛。参赛团队需在规定时间内解决实际问题,运用数学建模方法进行分析并提出解决方案。2021年第十一届比赛的D题就是一个典型例子。 MATLAB是解决这类问题的常用工具。它是一款强大的数值计算和编程软件,广泛应用于数学建模、数据分析和科学计算。MATLAB拥有丰富的函数库,涵盖线性代数、统计分析、优化算法、信号处理等多种数学操作,方便参赛者构建模型和实现算法。 在提供的文件列表中,有几个关键文件: d题论文(1).docx:这可能是参赛队伍对D题的解答报告,详细记录了他们对问题的理解、建模过程、求解方法和结果分析。 D_1.m、ratio.m、importfile.m、Untitled.m、changf.m、pailiezuhe.m、huitu.m:这些是MATLAB源代码文件,每个文件可能对应一个特定的计算步骤或功能。例如: D_1.m 可能是主要的建模代码; ratio.m 可能用于计算某种比例或比率; importfile.m 可能用于导入数据; Untitled.m 可能是未命名的脚本,包含临时或测试代码; changf.m 可能涉及函数变换; pailiezuhe.m 可能与矩阵的排列组合相关; huitu.m 可能用于绘制回路图或流程图。 matlab111.mat:这是一个MATLAB数据文件,存储了变量或矩阵等数据,可能用于后续计算或分析。 D-date.mat:这个文件可能包含与D题相关的特定日期数据,或是模拟过程中用到的时间序列数据。 从这些文件可以推测,参赛队伍可能利用MATLAB完成了数据预处理、模型构建、数值模拟和结果可视化等一系列工作。然而,具体的建模细节和解决方案需要查看解压后的文件内容才能深入了解。 在数学建模过程中,团队需深入理解问题本质,选择合适的数学模
以下是关于三种绘制云图或等高线图算法的介绍: 一、点距离反比插值算法 该算法的核心思想是基于已知数据点的值,计算未知点的值。它认为未知点的值与周围已知点的值相关,且这种关系与距离呈反比。即距离未知点越近的已知点,对未知点值的影响越大。具体来说,先确定未知点周围若干个已知数据点,计算这些已知点到未知点的距离,然后根据距离的倒数对已知点的值进行加权求和,最终得到未知点的值。这种方法简单直观,适用于数据点分布相对均匀的情况,能较好地反映数据在空间上的变化趋势。 二、双线性插值算法 这种算法主要用于处理二维数据的插值问题。它首先将数据点所在的区域划分为一个个小的矩形单元。当需要计算某个未知点的值时,先找到该点所在的矩形单元,然后利用矩形单元四个顶点的已知值进行插值计算。具体过程是先在矩形单元的一对对边上分别进行线性插值,得到两个中间值,再对这两个中间值进行线性插值,最终得到未知点的值。双线性插值能够较为平滑地过渡数据值,特别适合处理图像缩放、地理数据等二维场景中的插值问题,能有效避免插值结果出现明显的突变。 三、面距离反比 + 双线性插值算法 这是一种结合了面距离反比和双线性插值两种方法的算法。它既考虑了数据点所在平面区域对未知点值的影响,又利用了双线性插值的平滑特性。在计算未知点的值时,先根据面距离反比的思想,确定与未知点所在平面区域相关的已知数据点集合,这些点对该平面区域的值有较大影响。然后在这些已知点构成的区域内,采用双线性插值的方法进行进一步的插值计算。这种方法综合了两种算法的优点,既能够较好地反映数据在空间上的整体分布情况,又能保证插值结果的平滑性,适用于对插值精度和数据平滑性要求较高的复杂场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值