如何使用Elixir实现支持主动取消与回调自动取消的周期性定时器
Elixir 周期性定时器实现方案
原有代码的核心问题有两点:
- GenServer启动后的初始化逻辑位置错误:你把
Process.send_after的逻辑写在了启动GenServer的父进程中,定时器消息实际发给了父进程而非GenServer进程,状态也没有正确存储周期、回调函数两个核心参数 - 没有预留取消逻辑、回调执行逻辑的相关实现
完整调整后实现
defmodule PeriodicTimer do use GenServer # 对外暴露的API @doc "启动定时器,参数为周期(毫秒)、回调函数,返回GenServer进程pid用于后续操作" def start(period, callback) when is_integer(period) and period > 0 and is_function(callback, 0) do GenServer.start_link(__MODULE__, %{period: period, callback: callback}) end @doc "主动取消指定定时器" def cancel(timer_pid) do GenServer.call(timer_pid, :cancel) end # GenServer 内部回调 @impl true def init(state) do # 初始化时启动第一次定时器 timer = Process.send_after(self(), :run_callback, state.period) {:ok, Map.put(state, :timer_ref, timer)} end @impl true def handle_info(:run_callback, state) do # 执行传入的回调函数 callback_res = state.callback.() if callback_res == :cancel do # 回调返回:cancel时直接终止进程,自动清理剩余定时器 {:stop, :normal, state} else # 启动下一轮定时器 new_timer = Process.send_after(self(), :run_callback, state.period) {:noreply, %{state | timer_ref: new_timer}} end end @impl true def handle_call(:cancel, _from, state) do # 主动取消时先清理未触发的定时器 Process.cancel_timer(state.timer_ref) {:reply, :ok, state} end end
使用示例
# 示例1:每1秒执行一次,执行5次后自动终止 count = :counters.new(1, [:atomics]) {:ok, timer_pid} = PeriodicTimer.start(1000, fn -> current = :counters.get(count, 1) IO.puts("第#{current + 1}次执行") :counters.add(count, 1, 1) if current >= 4, do: :cancel, else: :ok end) # 示例2:主动取消定时器 # PeriodicTimer.cancel(timer_pid)
额外说明
- 上述实现支持同时启动多个独立的定时器,如果你需要全局单实例定时器,可以在
GenServer.start_link的参数中添加name: __MODULE__固定进程名 - 如果需要回调执行结束后再间隔指定周期启动下一轮,可以把
Process.send_after的逻辑移到回调执行完成之后再调用,避免回调执行时间过长导致的任务堆积
内容的提问来源于stack exchange,提问作者Mr. Engineer
相关产品推荐
相关产品推荐

