如何在 Elixir 或 Erlang 中使用 GenServer 实现可重置的倒数计时器

How to implement a resetable countdown timer with a GenServer in Elixir or Erlang

我们如何使用 GenServer 实现可重置的倒数计时器?

1) 在固定时间后执行任务,比如每 60 秒

2) 有办法在计时器结束前将倒计时重置回 60 秒

我看过 How to perform actions periodically with Erlang's gen_server?,但它没有完全涵盖在倒计时结束前让计时器休息的方面。

谢谢。

定义如何执行定期作业。

要以此为基础实现取消,您可以执行以下操作:

defmodule MyApp.Periodically do
  use GenServer

  def start_link(_name \ nil) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  def reset_timer() do
    GenServer.call(__MODULE__, :reset_timer)
  end

  def init(state) do
    timer = Process.send_after(self(), :work, 60_000)
    {:ok, %{timer: timer}}
  end

  def handle_call(:reset_timer, _from, %{timer: timer}) do
    :timer.cancel(timer)
    timer = Process.send_after(self(), :work, 60_000)
    {:reply, :ok, %{timer: timer}}
  end

  def handle_info(:work, state) do
    # Do the work you desire here

    # Start the timer again
    timer = Process.send_after(self(), :work,60_000)

    {:noreply, %{timer: timer}}
  end

  # So that unhanded messages don't error
  def handle_info(_, state) do
    {:ok, state}
  end
end

这维护了对计时器的引用,允许取消它。每次收到 :work 消息时,都会创建一个新的计时器并将其存储在 GenServer 的状态中。

如果您是在 Erlang 中完成的,根据您提到的其他问题...

您保存定时器引用并调用 erlang:cancel_timer/1 来阻止它触发(如果它还没有触发的话)。您必须注意这种 'fired already' 竞争条件,当您取消计时器时,触发消息已经在您的消息队列中。你可能关心也可能不关心这个,但是如果在触发器被取消后你永远不会执行操作很重要,那么你需要在设置定时消息时创建一个引用(或者你可以使用一个计数器),以及什么时候你得到一个触发器,你必须检查它是否与最新的触发器相关。

然后另一个问题的代码变成:

-define(INTERVAL, 60000). % One minute

init(Args) ->
    ...
    % Start first timer
    MyRef = erlang:make_ref(),
    {ok, TRef} = erlang:send_after(?INTERVAL, self(), {trigger, MyRef}),
    ...
    {ok, State#your_record{
        timer  = TRef,
        latest = MyRef
        }}.

% Trigger only when the reference in the trigger message is the same as in State
handle_info({trigger, MyRef}, State = #your_record{latest=MyRef}) ->
    % Do the action
    ...
    % Start next timer
    MyRef = erlang:make_ref(),
    {ok, TRef} = erlang:send_after(?INTERVAL, self(), trigger),
    ...
    {ok, State#your_record{
        timer  = TRef,
        latest = MyRef
        }}.

% Ignore this trigger, it has been superceeded!
handle_info({trigger, _OldRef}, State) ->
    {ok, State}.

像这样重置计时器:

handle_info(reset, State = #your_record{timer=TRef}) ->
    % Cancel old timer
    erlang:cancel_timer(TRef),
    % Start next timer
    MyNewRef = erlang:make_ref(),
    {ok, NewTRef} = erlang:send_after(?INTERVAL, self(), trigger),
    {ok, State#your_record{
        timer  = NewTRef,
        latest = MyNewRef
        }}.

调用可能更适合取消功能,但这取决于您的应用,因此由您决定。

从技术上讲,没有必要取消计时器,因为一旦你用新的引用创建了一个新状态,即使旧计时器继续运行,它在触发时也会被忽略,但我认为它是真的最好收拾一下。