Erlang Custom Timer implementation

  • Game server development timer design
  • Need to combine the development and implementation of the game framework to make choices

Reference

Design

timer process distribution

  • Independent timer process management and distribution of short message processing,
  • Reduce the use of receive / after in the system, that is, reduce the use of timer timers

gen_server timeout mechanism

  • Based on the gen_server timeout mechanism, use gb_trees to manage timer data
  • The business calculates the next trigger time according to the needs and adds it to the timer data

receive / after test

-module(mod_receive_after).

-export([
    test1/1,
    recev1/0,
    test2/1,
    recev2/0
]).

test1(Number) ->
    List = lists:seq(1, Number, 1),
    [spawn(?MODULE, recev1, []) || _ <- List],
    ok.

recev1() ->
    receive
        test -> stop
    end.

test2(Number) ->
    List = lists:seq(1, Number, 1),
    [spawn(?MODULE, recev2, []) || _ <- List],
    ok.

recev2() ->
    receive
        test -> stop
    after 1000 ->
        recev2()
    end.
    
  • Run test1
erl +P 2500000
mod_receive_after:test1(2000000).
ok
  • Run test2 【Modify after 1000 to after 10000】
erl +P 2500000
mod_receive_after:test2(2000000).
ok
  • After time is shorter, test2 battle uses more CPU
  • Timers require corresponding overhead

timer process distribution example

timer_server.erl

do_reg(PID) ->
    OldPIDList =
        case erlang:get(reg_pid_list) of
            [_ | _] = OldPIDListT -> OldPIDListT;
            _ -> []
        end,
    erlang:put(reg_pid_list, [PID | OldPIDList]),
    ok.

do_loop() ->
    PIDList =
        case erlang:get(reg_pid_list) of
            [_ | _] = PIDListT -> PIDListT;
            _ -> []
        end,
    do_loop(PIDList).

do_loop([]) ->
    ok;
do_loop([PID | PIDList]) ->
    erlang:send(PID, {loop_message}),
    do_loop(PIDList).
  • Register processes that need to be called cyclically
  • Use erlang:send_after/3 to execute the do_loop function on a regular basis
  • Send a scheduled message to all registered processes

gen_server timeout mechanism example

  • receive_after_server.erl
init([Timeout]) ->
    process_flag(trap_exit, true),
    case Timeout of
        infinity ->
            erlang:send(erlang:whereis(timer_server), {reg, erlang:self()}),
            {ok, #state{interval = Timeout}};
        _ ->
            {ok, #state{interval = Timeout}, Timeout}
    end.

handle_call(Request, _From, #state{interval = infinity} = State) ->
    {reply, Request, State};
handle_call(Request, _From, #state{interval = Timeout} = State) ->
    {reply, Request, State, Timeout}.

handle_cast(_Request, #state{interval = infinity} = State) ->
    {noreply, State};
handle_cast(_Request, #state{interval = Timeout} = State) ->
    {noreply, State, Timeout}.

handle_info({'EXIT', _, _Reason}, State) ->
    {stop, normal, State};
handle_info({loop_message}, #state{interval = infinity} = State) ->
    {noreply, State};
handle_info(_Info, #state{interval = infinity} = State) ->
    {noreply, State};
handle_info(timeout, #state{interval = Timeout, times = Times} = State) ->
    {noreply, State#state{times = Times + 1}, Timeout};
handle_info(_Info, State) ->
    {noreply, State}.
  • Timeout mechanism {ok, State, Timeout} triggers handle_info({timeout, State) message
  • After the process processes other messages, it needs to determine the next timeout time to implement timing

Source Code

  • timer_server.erl
-module(timer_server).
-behaviour(gen_server).

-export([
    start_link/0
]).
-export([
    init/1,
    handle_call/3,
    handle_cast/2,
    handle_info/2,
    terminate/2,
    code_change/3
]).

-record(state, {}).

%%%===================================================================
%%% Spawning and gen_server implementation
%%%===================================================================

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

init([]) ->
    process_flag(trap_exit, true),
    erlang:send_after(1000, erlang:self(), {loop, 1000}),
    {ok, #state{}}.

handle_call(Request, _From, State) ->
    {reply, Request, State}.

handle_cast(_Request, State) ->
    {noreply, State}.

handle_info({'EXIT', _, _Reason}, State) ->
    {stop, normal, State};
handle_info({reg, PID}, State) ->
    do_reg(PID),
    {noreply, State};
handle_info({loop, 1000}, State) ->
    erlang:send_after(1000, erlang:self(), {loop, 1000}),
    do_loop(),
    {noreply, State};
handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

do_reg(PID) ->
    OldPIDList =
        case erlang:get(reg_pid_list) of
            [_ | _] = OldPIDListT -> OldPIDListT;
            _ -> []
        end,
    erlang:put(reg_pid_list, [PID | OldPIDList]),
    ok.

do_loop() ->
    PIDList =
        case erlang:get(reg_pid_list) of
            [_ | _] = PIDListT -> PIDListT;
            _ -> []
        end,
    do_loop(PIDList).

do_loop([]) ->
    ok;
do_loop([PID | PIDList]) ->
    erlang:send(PID, {loop_message}),
    do_loop(PIDList).
  • receive_after_server.erl
-module(receive_after_server).
-behaviour(gen_server).

-export([
    start1/1,
    start2/1
]).
-export([
    init/1,
    handle_call/3,
    handle_cast/2,
    handle_info/2,
    terminate/2,
    code_change/3
]).

-record(state, {interval, times = 0}).

%%%===================================================================
%%% Spawning and gen_server implementation
%%%===================================================================

gen_name(I) ->
    erlang:list_to_atom(lists:concat([?MODULE, "_", I])).

start1(Number) ->
    List = lists:seq(1, Number, 1),
    [
        begin
            PName = gen_name(I),
            {ok, _} = gen_server:start_link({local, PName}, ?MODULE, [infinity], [])
        end
     || I <- List
    ],
    ok.

start2(Number) ->
    List = lists:seq(1, Number, 1),
    [
        begin
            PName = gen_name(I),
            {ok, _} = gen_server:start_link({local, PName}, ?MODULE, [1000], [])
        end
     || I <- List
    ],
    ok.

init([Timeout]) ->
    process_flag(trap_exit, true),
    case Timeout of
        infinity ->
            erlang:send(erlang:whereis(timer_server), {reg, erlang:self()}),
            {ok, #state{interval = Timeout}};
        _ ->
            {ok, #state{interval = Timeout}, Timeout}
    end.

handle_call(Request, _From, #state{interval = infinity} = State) ->
    {reply, Request, State};
handle_call(Request, _From, #state{interval = Timeout} = State) ->
    {reply, Request, State, Timeout}.

handle_cast(_Request, #state{interval = infinity} = State) ->
    {noreply, State};
handle_cast(_Request, #state{interval = Timeout} = State) ->
    {noreply, State, Timeout}.

handle_info({'EXIT', _, _Reason}, State) ->
    {stop, normal, State};
handle_info({loop_message}, #state{interval = infinity} = State) ->
    {noreply, State};
handle_info(_Info, #state{interval = infinity} = State) ->
    {noreply, State};
handle_info(timeout, #state{interval = Timeout, times = Times} = State) ->
    {noreply, State#state{times = Times + 1}, Timeout};
handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.
  • mod_receive_after.erl
-module(mod_receive_after).

-export([
    test1/1,
    recev1/0,
    test2/1,
    recev2/0
]).

test1(Number) ->
    List = lists:seq(1, Number, 1),
    [spawn(?MODULE, recev1, []) || _ <- List],
    ok.

recev1() ->
    receive
        test -> stop
    end.

test2(Number) ->
    List = lists:seq(1, Number, 1),
    [spawn(?MODULE, recev2, []) || _ <- List],
    ok.

recev2() ->
    receive
        test -> stop
    after 1000 ->
        recev2()
    end.

Test

Method A: timer process distribution

erl +P 2500000 +t 1073741824
timer_server:start_link().
receive_after_server:start1(2000).
ok

Method B: timeout mechanism

erl +P 2500000 +t 1073741824
receive_after_server:start2(2000).
ok

Conclusion

  • It is more convenient to develop business logic in the business module using the timing round-robin method

  • The business only needs to register the loop call method, and determine the next trigger time in the loop and execute it

  • The overall business framework is simpler