2026/11/9

erlang:hibernate/3

在 erlang OTP 的每一個 process 都有自己的 heap 與 stack,當有上萬個 process 時,如果 process 處於空閒等待狀態,會佔用很多記憶體。erlang:hibernate/3 的用途就是強制釋放 process 佔用的記憶體,進入休眠模式,直到真的收到下一個 message,會再被喚醒呼叫。

呼叫 erlang:hibernate 時,erlang 會進行下列步驟

  1. Garbage Collection,強制對該 process 進行一次 GC

  2. 釋放記憶體

    釋放 process 使用的 heap, stack,恢復到 process 最小化的狀態

  3. 休眠

    進入休眠模式,從 scheduler 的 queue 裡面移除,不在消耗 CPU

  4. 自動喚醒

    當收到新的 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 能節省記憶體,但也有對應的缺點

  1. CPU resource

    因為重新喚醒都需要重新設定 CPU 執行環境,且會觸發 GC,如果 process 常常收到訊息,hibernate 反而會讓系統變慢

  2. 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 壓縮記憶體...

沒有留言:

張貼留言