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

时间:2016-01-22 13:52:32

标签: timer elixir countdown gen-server

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

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

2)有一种方法可以在计时器过去之前将倒计时重置为60秒

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

感谢。

2 个答案:

答案 0 :(得分:11)

How to run some code every few hours in Phoenix framework?定义了如何定期工作。

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

defmodule MyApp.Periodically do
  use GenServer

  def start_link() 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的状态中。

答案 1 :(得分:4)

如果你在Erlang中这样做,根据你提到的另一个问题......

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

其他问题的代码变为:

-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
        }}.

拨打电话可能更适合取消功能,但这取决于您的应用,因此取决于您。

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