我的博客
  • Elixir的进程管理

Elixir的进程管理

因为身处的行业以保守而闻名,工作中经常使用的编程语言也是因为落后与守旧而臭名昭著的Java语言。 当初一直认为,相比很多现代的编程语言,Java虽然缺少很多语法糖,但是强大的生态足够让它在生产侧笑傲江湖。 前些日子,因为生产上遇到了问题, 尝试了各种方法都没有达到自己想要的效果, 但是这个问题或许在其他编程语言中根本不会存在。 这才觉察到学一门新的语言,相当于换一种活法,也许比想象中的要愉快很多。

我们在这个问题暴露出来的需求是什么呢? 是想要中断正在执行的任务,并且,这种中断的手段应该在具体的定时任务实现以外。

现在用Elixir一个简单的定时任务系统,先复现生产上的问题,然后再看看用Elixir如何解决它。

创建一个应用

创建一个有监督树的应用。

mix new hello_world --sup
defmodule HelloWorld.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Starts a worker by calling: HelloWorld.Worker.start_link(arg)
      # {HelloWorld.Worker, arg}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: HelloWorld.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

children里是将要被Supervisor启动的进程,而opts是进程树的配置参数 其中strategy的:one_for_one,的意思是,当一个进程崩溃后,只重启他自己。

为了还原现场,我们先实现一个死循环的方法,作为任务。

defmodule HelloWorld.Job do
    def infinite_hello(words) do
    Process.sleep(1000)
    IO.puts("Hello, #{words}!")
    infinite_hello(words)
  end
end

定时任务需要调度器。像是这种需要后台一直执行,存在状态的需求,用GenServer非常好实现。

在application.ex中,注册这个调度器,然后启动应用。

可以看到,控制台在不停的输出"Hello, World!"

我们希望能够终止掉这个行为,所以我们决定从最简单的开始,停止这个调度器。

defmodule HelloWorld.Scheduler do
  use GenServer

  def start_link(args) do
    GenServer.start_link(__MODULE__, args, name: __MODULE__)
  end

  def init(_args) do
    :timer.apply_after(1000, __MODULE__, :start_job, [])
    :timer.apply_after(10000, __MODULE__, :terminate_job, [])
    {:ok, %{}}
  end


  def terminate_job do
    GenServer.stop(__MODULE__, :normal)
  end

  @spec start_job() :: any()
  def start_job do
    GenServer.call(__MODULE__, :start_job)
  end

  def handle_call(:start_job, _from, state) do
    case state do
      %{pid: pid, ref: _ref} ->
        IO.puts("Job already running with PID: #{inspect(pid)}")
        {:reply, "Job already running", state}

      %{} ->
        {pid, ref} =
          spawn_monitor(fn ->
            HelloWorld.Job.infinite_hello("World")
          end)
        {:reply, "Job started", %{pid: pid, ref: ref}}
    end
  end

  def handle_info({:DOWN, ref, :process, pid, reason}, state) do
    if state.ref == ref do
      IO.puts("Process #{inspect(pid)} terminated with reason: #{inspect(reason)}")
      {:noreply, reason, state}
    else
      {:noreply, state}
    end
  end

  # 让调度器能能够停止
  def terminate(reason, state) do
    IO.puts("Scheduler terminating with reason: #{inspect(reason)}")
    case state do
      %{pid: pid, ref: _ref} ->
        Process.exit(pid, :normal)
        IO.puts("Job with PID #{inspect(pid)} terminated")
      _ ->
        :ok
    end
  end
end

我们用spawn_monitor启动一个子进程来运行我们的定时任务,在10s后停止这个调度器的同时,给子进程发送终止信号。

不过我们运行起来这个项目会发现,控制台的日志输出在10s内非但没有停止,而且每过10s都会新增一个线程不停的输出。 这是因为Supervisor在我们终止GenServer以后,它会有一次把它拉起来。 在init/1方法里又启动了一个子进程。

并且最重要的是, Process.exit(:normal)并不能让子进程结束,所以我们需要修改两个地方,让那个整个应用能够正常结束。

  1. Process.eixt(:normal)改成:kill
  2. 通过GenServer的chil_spec把自己设定成不会重启的任务
def child_spec(_args) do
  %{
    id: __MODULE__,
    start: {__MODULE__, :start_link, [[]]},
    restart: :temporary,
    type: :worker
  }
end
最近更新: 2026/3/15 14:17
Contributors: Keyang Li