當前位置: 首頁>>代碼示例 >>用法及示例精選 >>正文


Elixir GenServer用法及代碼示例


Elixir語言中 GenServer 相關用法介紹如下。

用於實現client-server 關係的服務器的行為模塊。

GenServer 是與任何其他 Elixir 進程一樣的進程,它可用於保持狀態、異步執行代碼等。使用使用此模塊實現的通用服務器進程 (GenServer) 的優點是它將具有一組標準的接口函數,並包括用於跟蹤和錯誤報告的函數。它也將適合監督樹。

示例

GenServer 行為抽象了常見的client-server 交互。開發人員隻需要實現他們感興趣的回調和函數。

讓我們從一個代碼示例開始,然後探索可用的回調。想象一下,我們想要一個像堆棧一樣工作的GenServer,允許我們推送和彈出元素:

defmodule Stack do
  use GenServer

  # Callbacks

  @impl true
  def init(stack) do
    {:ok, stack}
  end

  @impl true
  def handle_call(:pop, _from, [head | tail]) do
    {:reply, head, tail}
  end

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

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

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

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

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

我們通過調用 start_link/2 開始我們的 Stack,將模塊與服務器實現及其初始參數(表示包含元素 :hello 的堆棧的列表)一起傳遞。我們主要可以通過發送兩種類型的消息與服務器進行交互。調用消息期望來自服務器的回複(因此是同步的),而轉換消息則不需要。

每次執行 GenServer.call/3 時,客戶端都會發送一條消息,該消息必須由 GenServer 中的 handle_call/3 回調處理。 cast/2 消息必須由 handle_cast/2 處理。當您使用 GenServer 時,有 8 個可能的回調被實現。唯一需要的回調是 init/1

客戶端/服務器 API

雖然在上麵的例子中我們已經使用 GenServer.start_link/3 和朋友直接啟動並與服務器通信,但大多數時候我們並沒有直接調用 GenServer 函數。相反,我們將調用包裝在代表服務器公共 API 的新函數中。

這是我們的 Stack 模塊的一個更好的實現:

defmodule Stack do
  use GenServer

  # Client

  def start_link(default) when is_list(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(stack) do
    {:ok, stack}
  end

  @impl true
  def handle_call(:pop, _from, [head | tail]) do
    {:reply, head, tail}
  end

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

實際上,在同一個模塊中同時具有服務器和客戶端函數是很常見的。如果服務器和/或客戶端實現變得越來越複雜,您可能希望將它們放在不同的模塊中。

如何監督

GenServer 最常在監督樹下啟動。當我們調用 use GenServer 時,它會自動定義一個 child_spec/1 函數,允許我們直接在主管下啟動 Stack。要在主管下啟動 [:hello] 的默認堆棧,可以執行以下操作:

children = [
  {Stack, [:hello]}
]

Supervisor.start_link(children, strategy: :one_for_all)

請注意,您也可以將其簡單地啟動為 Stack ,這與 {Stack, []} 相同:

children = [
  Stack # The same as {Stack, []}
]

Supervisor.start_link(children, strategy: :one_for_all)

在這兩種情況下,總是調用Stack.start_link/1

use GenServer 還接受一個選項列表,用於配置子規範以及它如何在主管下運行。生成的child_spec/1 可以使用以下選項進行自定義:

  • :id - 子規範標識符,默認為當前模塊
  • :restart - 當孩子應該重新啟動時,默認為 :permanent
  • :shutdown - 如何立即關閉孩子,或者讓孩子有時間關閉

例如:

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

有關更多詳細信息,請參閱 Supervisor 模塊中的"Child specification" 部分。緊接在use GenServer 之前的@doc 注釋將附加到生成的child_spec/1 函數。

名稱注冊

start_link/3 start/3 都支持 GenServer 通過 :name 選項在啟動時注冊名稱。注冊名稱也會在終止時自動清理。支持的值為:

  • 一個原子 - GenServer 使用給定名稱在本地(到當前節點)注冊 Process.register/2

  • {:global, term}- GenServer 使用給定術語全局注冊:global 模塊.

  • {:via, module, term}- GenServer 使用給定的機製和名稱注冊。這:via選項需要一個導出的模塊register_name/2,unregister_name/1,whereis_name/1Kernel.send.一個這樣的例子是:global 模塊它使用這些函數來保存進程名稱列表及其關聯的 PID,這些 PID 可用於 Elixir 節點網絡的全局。 Elixir 還附帶了一個本地的、分散的和可擴展的注冊表,稱為Registry用於本地存儲動態生成的名稱。

例如,我們可以在本地啟動並注冊我們的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

一旦服務器啟動,此模塊中的其餘函數( call/3 cast/2 和朋友)也將接受一個原子,或任何 {:global, ...}{:via, ...} 元組。通常,支持以下格式:

  • PID
  • 如果服務器在本地注冊,則為 atom
  • {atom, node} 如果服務器在另一個節點本地注冊
  • {:global, term} 如果服務器是全局注冊的
  • {:via, module, name} 如果服務器是通過備用注冊表注冊的

如果有興趣在本地注冊動態名稱,請不要使用原子,因為原子永遠不是garbage-collected,因此動態生成的原子不會是garbage-collected。對於這種情況,您可以使用 Registry 模塊設置您自己的本地注冊表。

接收"regular" 消息

GenServer 的目標是為開發人員抽象"receive" 循環,自動處理係統消息,支持代碼更改,同步調用等。因此,您永遠不應在GenServer 回調中調用您自己的"receive",因為這樣做會導致GenServer 行為不端。

除了 call/3 cast/2 提供的同步和異步通信之外, Kernel.send/2 Process.send_after/4 等函數發送的"regular"消息也可以在 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() 值的使用如下:

  • 如果在返回timeout() 值時進程有任何消息已在等待,則超時將被忽略並照常處理等待消息。這意味著即使 0 毫秒的超時也不能保證執行(如果您想立即無條件地執行另一個操作,請改用 :continue 指令)。

  • 如果任何消息在指定的毫秒數過去之前到達,則超時被清除並照常處理該消息。

  • 否則,當指定的毫秒數過去後沒有消息到達時,將調用handle_info/2,並將:timeout 作為第一個參數。

何時(不)使用GenServer

到目前為止,我們已經了解到 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/2 回調來更改此狀態的表示方式。
  • :sys.trace/3 - 將所有係統事件打印到 :stdio
  • :sys.statistics/3 - 管理進程統計信息的收集。
  • :sys.no_debug/2 - 關閉給定進程的所有調試處理程序。完成後關閉調試非常重要。過多的調試處理程序或那些應該關閉但沒有關閉的處理程序會嚴重損害係統的性能。
  • :sys.suspend/2 - 允許暫停一個進程,以便它隻回複係統消息而不回複其他消息。可以通過 :sys.resume/2 重新激活暫停的進程。

讓我們看看如何使用這些函數來調試我們之前定義的堆棧服務器。

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

iex> :sys.get_state(pid)
[1]

iex> 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

iex> :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
 ]}

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

iex> :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]}]
   ]
 ]}

學到更多

如果您想了解有關 GenServers 的更多信息,Elixir 入門指南提供了tutorial-like 介紹。 Erlang 中的文檔和鏈接也可以提供額外的見解。

相關用法


注:本文由純淨天空篩選整理自elixir-lang.org大神的英文原創作品 GenServer。非經特殊聲明,原始代碼版權歸原作者所有,本譯文未經允許或授權,請勿轉載或複製。