概述
如果我們需要管理多個程式,那麼,就需要一個專門的 server 來集中監控和控制這些程式的狀態,啟停等。
OTP 平臺中的 GenServer 就是對這個 server 通用部分的抽象。
利用 GenServer 中已經提供的通用操作, 可以很方便的開發出可靠,健壯的程式。
下面首先通過一個示例演示 GenServer 的方便和強大之處,然後再對其進行介紹。
GenServer 示例
這是一個 GenServer 管理多個程式的示例,模擬控制各個程式的啟動,停止,以及狀態查詢。
defmodule ProcessMonitor do
use GenServer
#====================================================
# api for clients
#====================================================
# start GenServer
def start(data, opt \\ []) do
GenServer.start_link(__MODULE__, data, opt)
end
# add process which is controled by this GenServer
def process_add(server, name) do
GenServer.call(server, {:add, name})
end
# get process status
def process_status(server, name) do
GenServer.call(server, {:status, name})
end
# start a process by name
def process_start(server, name) do
GenServer.cast(server, {:start, name})
end
# stop a process by name
def process_stop(server, name) do
GenServer.cast(server, {:stop, name})
end
#====================================================
# callbacks for server
#====================================================
def init(data) do
{:ok, data}
end
# handle status message synchronization
def handle_call({:status, name}, _from, data) do
val = Map.get(data, name, nil)
{:reply, val, data}
end
# handle add message synchronization
def handle_call({:add, name}, _from, data) do
data = Map.put(data, name, "stopped")
{:reply, name, data}
end
# handle start message asynchronization
def handle_cast({:start, name}, data) do
data = Map.put(data, name, "running")
{:noreply, data}
end
# handle stop message asynchronization
def handle_cast({:stop, name}, data) do
data = Map.put(data, name, "stopped")
{:noreply, data}
end
end
上面程式碼測試方法如下:
$ iex -S mix
Erlang/OTP 18 [erts-7.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (1.2.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, server} = ProcessMonitor.start(Map.new) # 建立 GenServer,並初始化一個 map 用於儲存此server管理的 process 資訊
{:ok, #PID<0.87.0>}
iex(2)> ProcessMonitor.process_status(server, "process01") # 建立 GenServer 後,預設沒有管理任何程式,所以沒有 process01 的資訊
nil
iex(3)> ProcessMonitor.process_add(server, "process01") # 給 GenServer 增加一個被管理程式 process01
"process01"
iex(4)> ProcessMonitor.process_status(server, "process01") # 新加入的程式預設狀態是 stopped,示例程式碼預設這麼實現
"stopped"
iex(5)> ProcessMonitor.process_start(server, "process01") # 啟動 process01
:ok
iex(6)> ProcessMonitor.process_status(server, "process01") # process01 狀態變為 running
"running"
iex(7)> ProcessMonitor.process_stop(server, "process01") # 停止 process01
:ok
iex(8)> ProcessMonitor.process_status(server, "process01") # process01 狀態變為 stopped
"stopped"
iex(9)> ProcessMonitor.process_add(server, "process02") # 再增加一個被管理程式 process02
"process02"
iex(10)> ProcessMonitor.process_start(server, "process02") # 啟動 process02
:ok
iex(11)> ProcessMonitor.process_status(server, "process02") # process02 狀態變為 running
"running"
iex(12)> ProcessMonitor.process_status(server, "process01") # process01 狀態仍然是 stopped,不受 process02 的影響
"stopped"
iex(13)> ProcessMonitor.stop(server) # 停止 GenServer
注 上面的程式碼是用 mix 建立工程來執行的,mix 的使用方法可以參見 blog:mix 構建工具
GenServer 通用抽象簡介
示例程式碼使用了 GenServer 中的幾個關鍵函式: init handle_call handle_case
- init: 這個函式在 GenServer.start_link 時執行,對 start_link 中的引數進行處理
- handle_call: 這個函式接受同步訊息並處理
- handle_cast: 這個函式接受非同步訊息並處理
處理這3個常用的函式之外,GenServer 中的函式也不是很多,其他的函式,屬性以及每個函式返回的值說明請參見:http://elixir-lang.org/docs/stable/elixir/GenServer.html
在上面的示例中,其實 client 也可以直接呼叫 GenServer 的 handle_call/handle_cast 來傳送同步/非同步訊息,
我之所以封裝了一些 api 給 client 呼叫,一方面,是為了簡化客戶端的呼叫(client 的 api 中引數更加簡潔直觀),
另一方面,將處理訊息的程式碼和 傳送訊息的程式碼分開,便於以後擴充套件(因為,可能存在多個傳送訊息的處理都對應同一個訊息處理)。