构建GenServer调用self的正确方法

Mic*_*cah 10 elixir gen-server

我知道让GenServer进程调用本身几乎是不可能的,因为你基本上遇到了死锁.但是,我很好奇是否有一种首选方式来做这种事情.

假设以下场景:我有一个队列,我正在弹出的东西.如果队列是空的,我想重新填充它.我可以像这样构造它:

def handle_call(:refill_queue, state) do
  new_state = put_some_stuff_in_queue(state)
  {:reply, new_state}
end

def handle_call(:pop, state) do
  if is_empty_queue(state) do
    GenServer.call(self, :refill_queue)
  end

  val,new_state = pop_something(state)

  {:reply, val, new_state}
end
Run Code Online (Sandbox Code Playgroud)

这里的一个大问题是,当我们尝试重新填充队列时,这将会死锁.我过去使用的一个解决方案是使用cast更多,因此它不会死锁.像这样(的变化callcast进行补充)

def handle_cast(:refill_queue, state) do
Run Code Online (Sandbox Code Playgroud)

但在这种情况下,我认为它不起作用,因为pop在实际填充队列之前,异步转换重新填充队列可能会返回,这意味着我将尝试弹出一个空队列.

无论如何,核心问题是:处理这个问题的最佳方法什么?我假设答案是put_some_stuff_in_queue直接在pop通话中打电话,但我想检查一下.换句话说,它似乎是做正确的事是使handle_callhandle_cast尽可能简单,基本上只是包装来在实际工作情况等功能.然后,创建尽可能多的handle_*函数来覆盖您将要处理的所有可能情况,而不是handle_call(:foo)依次调用handle_call(:bar).

Ale*_*usa 8

GenServer模块中有一个函数叫做reply/2.handle_call/3回调的第二个参数是与客户端的连接.您可以创建一个新进程来处理连接并返回{:noreply, state}callback子句.使用你的例子:

defmodule Q do
  use GenServer

  ############
  # Public API

  def start_link do
    GenServer.start_link(__MODULE__, [])
  end

  def push(pid, x) do
    GenServer.call(pid, {:push, x})
  end

  def pop(pid) do
    GenServer.call(pid, :pop)
  end

  ########
  # Helper

  # Creates a new process and does a request to
  # itself with the message `:refill`. Replies
  # to the client using `from`.
  defp refill(from) do
    pid = self()
    spawn_link fn ->
      result = GenServer.call(pid, :refill)
      GenServer.reply(from, result)
    end
  end

  ##########
  # Callback

  def handle_call(:refill, _from, []) do
    {:reply, 1, [2, 3]}
  end
  def handle_call(:refill, _from, [x | xs]) do
     {:reply, x, xs}
  end
  def handle_call({:push, x}, _from, xs) when is_list(xs) do
    {:reply, :ok, [x | xs]}
  end
  def handle_call(:pop, from, []) do
    # Handles refill and the reply to from.
    refill(from)
    # Returns nothing to the client, but unblocks the
    # server to get more requests.
    {:noreply, []}
  end
  def handle_call(:pop, _from, [x | xs]) do
    {:reply, x, xs}
  end
end
Run Code Online (Sandbox Code Playgroud)

你会得到以下内容:

iex(1)> {:ok, pid} = Q.start_link()
{:ok, #PID<0.193.0>}
iex(2)> Q.pop(pid)
1
iex(3)> Q.pop(pid)
2
iex(4)> Q.pop(pid)
3
iex(5)> Q.pop(pid)
1
iex(6)> Q.pop(pid)
2
iex(7)> Q.pop(pid)
3
iex(8)> Q.push(pid, 4)
:ok
iex(9)> Q.pop(pid)    
4
iex(10)> Q.pop(pid)
1
iex(11)> Q.pop(pid)
2
iex(12)> Q.pop(pid)
3
iex(13)> tasks = for i <- 1..10 do
...(13)>   Task.async(fn -> {"Process #{inspect i}", Q.pop(pid)} end)
...(13)> end
(...)
iex(14)> for task <- tasks, do: Task.await(task)
[{"Process 1", 1}, {"Process 2", 2}, {"Process 3", 1}, {"Process 4", 2},
 {"Process 5", 3}, {"Process 6", 3}, {"Process 7", 2}, {"Process 8", 1},
 {"Process 9", 1}, {"Process 10", 3}]
Run Code Online (Sandbox Code Playgroud)

因此,实际上GenServer可以向自己发出请求.你只需要知道如何.

我希望这有帮助.

  • 我不知道你以后可以回复,而你处理的其他消息很棒.但!这是不是可以让多个pop调用同时调用笔芯?我的意思是,如果重新填充需要10毫秒,并且我们每3分钟进行一次弹出调用,我们将获得2次弹出(每次触发一次重新填充),而第一次是重新填充.因为该重新填充将转到消息队列,要在消息队列中已经存在的pops之后进行处理,对吗? (3认同)
  • @IsmaelAbreu是的,你是对的.您在我的代码中发现了一个错误.如果消息队列中有两个弹出窗口,则会触发两次重新填充.第一次补充将重新填充堆栈.第二个refill将找不到有效的子句,因为堆栈不为空,因此服务器将与`FunctionClauseError`崩溃.我修复了错误,并在重新填充找到非空堆栈时添加了新子句.谢谢你:D (2认同)