构建 GenServer 调用自身的正确方法
Proper way to structure GenServer calls to self
我知道让 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
这里最大的问题是当我们尝试重新填充队列时这会死锁。我过去使用的一种解决方案是多使用 cast
,这样它就不会死锁。像这样(将 call
更改为 cast
以重新填充)
def handle_cast(:refill_queue, state) do
但在这种情况下,我认为它不会起作用,因为在 pop
情况下,重新填充队列的异步转换可能 return 在实际填充队列之前,这意味着我会尝试弹出一个空队列。
无论如何,核心问题是:处理这个问题的最佳方法是什么?我认为答案是直接在 [=15] 中调用 put_some_stuff_in_queue
=] 打电话,但我想检查一下。换句话说,似乎正确的做法是使 handle_call
和 handle_cast
尽可能简单,并且基本上只是对实际工作发生的其他函数的包装。然后,根据需要创建尽可能多的 handle_*
函数来涵盖您将处理的所有可能情况,而不是让 handle_call(:foo)
依次调用 handle_call(:bar)
.
为什么要制作GenServer.call?
def handle_call(:pop, state) do
new_state0 = if is_empty_queue(state) do
put_some_stuff_in_queue(state)
else
state
end
{val,new_state} = pop_something(new_state0)
{:reply, val, new_state}
end
或
def handle_call(:pop, state) do
{val, new_state} = state
|> is_empty_queue
|> case do
true ->
put_some_stuff_in_queue(state)
false ->
state
end
|> pop_something
{:reply, val, new_state}
end
所以调用是不行的,但是调用其他函数是完全可行的。
GenServer
模块中有一个名为 reply/2
的函数。 handle_call/3
回调的第二个参数是与客户端的连接。您可以创建一个新进程来处理回调子句中的连接和 return {:noreply, state}
。使用您的示例:
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
你会得到以下信息:
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}]
因此实际上GenServer 可以向自己发出请求。你只需要知道怎么做。
希望对您有所帮助。
我知道让 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
这里最大的问题是当我们尝试重新填充队列时这会死锁。我过去使用的一种解决方案是多使用 cast
,这样它就不会死锁。像这样(将 call
更改为 cast
以重新填充)
def handle_cast(:refill_queue, state) do
但在这种情况下,我认为它不会起作用,因为在 pop
情况下,重新填充队列的异步转换可能 return 在实际填充队列之前,这意味着我会尝试弹出一个空队列。
无论如何,核心问题是:处理这个问题的最佳方法是什么?我认为答案是直接在 [=15] 中调用 put_some_stuff_in_queue
=] 打电话,但我想检查一下。换句话说,似乎正确的做法是使 handle_call
和 handle_cast
尽可能简单,并且基本上只是对实际工作发生的其他函数的包装。然后,根据需要创建尽可能多的 handle_*
函数来涵盖您将处理的所有可能情况,而不是让 handle_call(:foo)
依次调用 handle_call(:bar)
.
为什么要制作GenServer.call?
def handle_call(:pop, state) do
new_state0 = if is_empty_queue(state) do
put_some_stuff_in_queue(state)
else
state
end
{val,new_state} = pop_something(new_state0)
{:reply, val, new_state}
end
或
def handle_call(:pop, state) do
{val, new_state} = state
|> is_empty_queue
|> case do
true ->
put_some_stuff_in_queue(state)
false ->
state
end
|> pop_something
{:reply, val, new_state}
end
所以调用是不行的,但是调用其他函数是完全可行的。
GenServer
模块中有一个名为 reply/2
的函数。 handle_call/3
回调的第二个参数是与客户端的连接。您可以创建一个新进程来处理回调子句中的连接和 return {:noreply, state}
。使用您的示例:
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
你会得到以下信息:
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}]
因此实际上GenServer 可以向自己发出请求。你只需要知道怎么做。
希望对您有所帮助。