在 erlang OTP 的每一個 process 都有自己的 heap 與 stack,當有上萬個 process 時,如果 process 處於空閒等待狀態,會佔用很多記憶體。erlang:hibernate/3 的用途就是強制釋放 process 佔用的記憶體,進入休眠模式,直到真的收到下一個 message,會再被喚醒呼叫。
呼叫 erlang:hibernate 時,erlang 會進行下列步驟
Garbage Collection,強制對該 process 進行一次 GC
釋放記憶體
釋放 process 使用的 heap, stack,恢復到 process 最小化的狀態
休眠
進入休眠模式,從 scheduler 的 queue 裡面移除,不在消耗 CPU
自動喚醒
當收到新的 message 時,會呼叫指定的模組函數
{Module, Function, Args},重新開始執行,並將 message 放入 mailbox 等待處理
最常見的應用是 server loop,當 process 進入 receive 等待時,如果預期會等待很久,就可以呼叫 hibernate 節省資源
-module(my_server).
-export([start/0, loop/0]).
%% 啟動一個簡單的處理循環
start() ->
spawn(fun() -> loop() end).
loop() ->
receive
{process_data, Data} ->
%% 執行繁重的運算
Result = heavy_computation(Data),
io:format("Result: ~p~n", [Result]),
%% 處理完後,不直接進入 loop(),而是進入休眠
%% 這樣當下一次有訊息進來,會自動呼叫 my_server:loop([])
erlang:hibernate(?MODULE, loop, []);
stop ->
ok
end.
heavy_computation(Data) ->
%% 模擬繁重工作
timer:sleep(1000),
Data * 2.
等待很久的意思,是類似 MQTT 客戶端,在長時間連線下等待新的訊息資料。或是有上萬個 process 在監控硬體狀態,但大部分的時間,狀態都沒有變化
雖然 hibernate 能節省記憶體,但也有對應的缺點
CPU resource
因為重新喚醒都需要重新設定 CPU 執行環境,且會觸發 GC,如果 process 常常收到訊息,hibernate 反而會讓系統變慢
context
因需要指定固定的
{Module, Function, Args},這表示程式的設計,必須要是 stateless,或是把狀態資料放在 Args 裡面
以下這個例子,就是將 Count 數值,透過 Args 參數傳送到下一次執行的 function 裡面
-module(counter_server).
-export([start/0, init/1]).
start() ->
spawn(?MODULE, init, [0]). %% 初始狀態為 0
init(Count) ->
loop(Count).
loop(Count) ->
receive
increment ->
NewCount = Count + 1,
io:format("Count is now: ~p~n", [NewCount]),
%% 帶領新狀態進入休眠
%% 醒來後會自動呼叫 counter_server:loop([NewCount])
erlang:hibernate(?MODULE, loop, [NewCount]);
{get_count, From} ->
From ! {count, Count},
%% 查詢不改變狀態,同樣可以休眠以節省資源
erlang:hibernate(?MODULE, loop, [Count]);
stop ->
ok
end.
gen_server 如果要支援 hibernate,只需要在回傳的資料加上 hibernate。
在 handle_call 或 handle_cast 的回傳值中,最後一個元素加上 hibernate ,這就是告訴 gen_server 要把訊息回傳後,並對此 process 處理一次 erlang:hibernate/3。
雖然進入了 hibernate,但 #state{} 裡面的 data ,會自動被保存,下次呼叫 get_value 時,該 process 會自動被喚醒,並能正確取得 state 裡面的 data 資料。
在一般的 gen_server 框架下,不需要手動檢查 Mailbox,因為框架已經幫你處理好了。也就是框架會自動檢查 mailbox,如果還有訊息,就不會進入 hibernate。
-module(settings_cache).
-behaviour(gen_server).
%% API
-export([start_link/0, set_value/2, get_value/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
-define(SERVER, ?MODULE).
%% 狀態定義
-record(state, {
data = #{} :: map()
}).
%% --- API 介面 ---
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% 非同步更新設定
set_value(Key, Value) ->
gen_server:cast(?SERVER, {set, Key, Value}).
%% 同步取得設定
get_value(Key) ->
gen_server:call(?SERVER, {get, Key}).
%% --- Callback 實作 ---
init([]) ->
%% 初始化時也可以選擇直接進入 hibernate
{ok, #state{}, hibernate}.
handle_call({get, Key}, _From, State = #state{data = Map}) ->
Reply = maps:get(Key, Map, undefined),
%% 回傳結果後進入 hibernate,釋放堆疊與壓縮堆積
{reply, Reply, State, hibernate};
handle_call(_Request, _From, State) ->
{reply, ok, State, hibernate}.
handle_cast({set, Key, Value}, State = #state{data = Map}) ->
NewMap = maps:put(Key, Value, Map),
%% 更新狀態後進入 hibernate
{noreply, State#state{data = NewMap}, hibernate};
handle_cast(_Msg, State) ->
{noreply, State, hibernate}.
handle_info(_Info, State) ->
%% 收到非預期訊息(例如喚醒訊息)處理完後繼續休眠
{noreply, State, hibernate}.
也可以加上一個延遲,短暫暫停,讓框架自動檢查這一段時間有沒有新的訊息。這樣才不需要一直頻繁檢查 mailbox size
handle_info(timeout, State) ->
{noreply, State, hibernate}; %% 只有在一段時間沒封包時才休眠
handle_cast(Msg, State) ->
%% 正常處理,並設定 5000 毫秒後觸發 timeout
{noreply, NewState, 5000}.
在 {noreply, NewState, 5000} 後,5000ms 內,如果又收到新的 message,gen_server 會自動取消 timeout
修改gen_server 套用 timeout 機制
-module(settings_cache).
-behaviour(gen_server).
%% API
-export([start_link/0, set_value/2, get_value/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
-define(SERVER, ?MODULE).
-define(IDLE_TIMEOUT, 5000). %% 定義閒置 5 秒後休眠
-record(state, {
data = #{} :: map()
}).
%% --- API 介面 ---
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
set_value(Key, Value) ->
gen_server:cast(?SERVER, {set, Key, Value}).
get_value(Key) ->
gen_server:call(?SERVER, {get, Key}).
%% --- Callback 實作 ---
init([]) ->
%% 初始化後等待 5 秒,若無動作則休眠
{ok, #state{}, ?IDLE_TIMEOUT}.
handle_call({get, Key}, _From, State = #state{data = Map}) ->
Reply = maps:get(Key, Map, undefined),
%% 回傳結果並重新計時 5 秒
%% 如果 5 秒內又有 call/cast,此計時器會被重置
{reply, Reply, State, ?IDLE_TIMEOUT};
handle_call(_Request, _From, State) ->
{reply, ok, State, ?IDLE_TIMEOUT}.
handle_cast({set, Key, Value}, State = #state{data = Map}) ->
NewMap = maps:put(Key, Value, Map),
%% 更新狀態後重新計時 5 秒
{noreply, State#state{data = NewMap}, ?IDLE_TIMEOUT}.
%% 關鍵修改:處理逾時訊息
handle_info(timeout, State) ->
%% 只有在連續 ?IDLE_TIMEOUT 毫秒沒收到訊息時才會執行到這裡
io:format("進程已閒置 ~p ms,執行 hibernate 壓縮記憶體...~n", [?IDLE_TIMEOUT]),
{noreply, State, hibernate};
handle_info(_Info, State) ->
%% 收到其他訊息(如原生訊息)後也重新計時
{noreply, State, ?IDLE_TIMEOUT}.
測試
erlc settings_cache.erl
過程中發現,如果間隔超過 5s 沒有呼叫 settings_cache function,就會呼叫 timeout
> erl
1> l(settings_cache).
{module,settings_cache}
2> settings_cache:start_link().
{ok,<0.87.0>}
進程已閒置 5000 ms,執行 hibernate 壓縮記憶體...
3> settings_cache:set_value(<<"key1">>, <<"value1">>).
ok
進程已閒置 5000 ms,執行 hibernate 壓縮記憶體...
4> settings_cache:get_value(<<"key1">>).
<<"value1">>
進程已閒置 5000 ms,執行 hibernate 壓縮記憶體...
5> settings_cache:set_value(<<"key2">>, <<"value2">>).
ok
6> settings_cache:set_value(<<"key1">>, <<"value1">>).
ok
7> settings_cache:get_value(<<"key1">>).
<<"value1">>
8> settings_cache:get_value(<<"key2">>).
<<"value2">>
進程已閒置 5000 ms,執行 hibernate 壓縮記憶體...
9> settings_cache:get_value(<<"key2">>).
<<"value2">>
進程已閒置 5000 ms,執行 hibernate 壓縮記憶體...
沒有留言:
張貼留言