我正在尝试将一个进程用作同步机制,它可以接收我们的订单消息,但仍能正常运行。我已经设法使用普通进程实现我的问题的简化版本,但是我无法与GenServer实现相同的效果。
简化版本是这样的:
defmodule Fun do
def start_link do
spawn(fn -> loop(:initiated) end)
end
def loop(state) do
receive do
:join when state == :initiated ->
IO.inspect("Handling join")
loop(:initiated)
:finish when state == :initiated ->
IO.inspect("Finishing")
loop(:finishing)
:cleanup when state == :finishing ->
IO.inspect("Cleaning up...")
end
end
end
上述内容仅在:join
为:finish
时处理state
和:initiated
条消息,仅执行:cleanup
并在:finish
时退出已收到。在这里,我试图利用邮件卡在邮箱中,直到它们匹配为止。
它的工作原理如下:
iex(1)> pid = Fun.start_link
#PID<0.140.0>
iex(2)> send(pid, :join)
"Handling join"
:join
iex(3)> send(pid, :join)
"Handling join"
:join
iex(4)> send(pid, :cleanup) # No `IO.inspect` at this point
:cleanup
iex(5)> send(pid, :finish) # Two `IO.inspect`s once `:finish` received
"Finishing"
:finish
"Cleaning up..."
我试图用GenServer重现相同的行为:
defmodule FunServer do
use GenServer
def start_link do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def init(:ok) do
{:ok, :initiated}
end
def handle_info(:join, msg) when msg == :initiated do
IO.inspect("Handling join [From GenServer]")
{:noreply, :initiated}
end
def handle_info(:finish, msg) when msg == :initiated do
IO.inspect("Finishing [From GenServer]")
{:noreply, :finishing}
end
def handle_info(:cleanup, msg) when msg == :finishing do
IO.inspect("Cleaning up [From GenServer]")
{:stop, :normal, msg}
end
end
鉴于我使用此GenServer将我的应用程序配置为:temporary
worker
,它的工作原理如下:
iex(1)> send(FunServer, :join)
"Handling join [From GenServer]"
:join
iex(2)> send(FunServer, :cleanup)
:cleanup
iex(3)>
07:11:17.383 [error] GenServer FunServer terminating
** (FunctionClauseError) no function clause matching in
FunServer.handle_info/2
(what_the_beam) lib/fun_server.ex:22: FunServer.handle_info(:cleanup, :initiated)
(stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:686: :gen_server.handle_msg/6
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: :cleanup
State: :initiated
我尝试过使用handle_cast
回调,以及不同格式的参数,例如:
handle_info(:cleanup, :finishing)
或
handle_cast(:cleanup, :finishing)
而不是where msg == :finishing
,但它们都不适合我。
我正在使用Elixir 1.5和Erlang 20。
答案 0 :(得分:2)
在原始代码中,当状态为receive
时,:cleanup
会忽略:initiated
消息。这是一个不好的做法,可能导致瓶颈,因为任何此类消息将永远留在进程的收件箱中,耗尽内存并减慢未来的receive
块,因为receive
(通常)花费时间与进程收件箱中的邮件数量成比例。
GenServer
通过强制您按照收到的顺序处理邮件来正确处理此案例。为了忽略消息,您需要显式添加不执行任何操作的handle_info
。您可以简单地添加此子句,当状态为:cleanup
时,将忽略:initiated
:
def handle_info(:cleanup, :initiated), do: {:noreply, :initiated}
您也可以通过在所有现有handle_info
之后添加此子句来忽略任何其他消息:
def handle_info(_message, state), do: {:noreply, state}
我尝试过使用handle_cast回调,以及不同格式的参数,例如:
handle_info(:cleanup, :finishing)
...
如果您仔细阅读错误消息中的堆栈跟踪,问题是:gen_server
试图调用FunServer.handle_info(:cleanup, :initiated)
,但handle_info
中没有定义任何条款来处理它。 (:cleanup, :finishing)
没有问题。
答案 1 :(得分:0)
您遇到的问题称为选择性接收常规erlang进程。这是Joe Armstrong的书中的引用:
receive works as follows:
...
2. Take the first message in the mailbox and try to match it
against Pattern1, Pattern2, and so on. If the match succeeds,
the message is removed from the mailbox, and the expressions
following the pattern are evaluated.
3. If none of the patterns in the receive statement matches the
first message in the mailbox, then the first message is removed
from the mailbox and put into a “save queue.” The second message
in the mailbox is then tried. This procedure is repeated until a
matching message is found or until all the messages in the mail-
box have been examined.
4. If none of the messages in the mailbox matches, then the process
is suspended and will be rescheduled for execution the next time
a new message is put in the mailbox. Note that when a new message
arrives, the messages in the save queue are not rematched; only
the new message is matched.
5. As soon as a message has been matched, then all messages that
have been put into the save queue are reentered into the mailbox
in the order in which they arrived at the process. If a timer
was set, it is cleared.
6. If the timer elapses when we are waiting for a message, then
evaluate the expressions ExpressionsTimeout and put any saved
messages back into the mailbox in the order in which they
arrived at the process.
gen_server不是这样工作的。它需要你的回调模块来匹配一条消息,或者你发现有一个错误抛出,因为gen_server实现没有找到一种分派方法。如果您希望gen_server实现与上面概述的接收逻辑相匹配,则必须手动执行。一种简单的方法是在某种列表中累积您在给定状态下无法匹配的所有消息,然后在每次成功匹配后将它们重新发送到self()。为此,你的状态不再仅仅是一个简单的原子,因为你需要自己组织保存的队列。
而BTW之前在erlang的背景下提出了同样的问题。响应者有一个与我描述的一对一的建议。因此,如果您需要具体代码,请链接到该问题:How do you do selective receives in gen_servers?
答案 2 :(得分:0)
使用:gen_statem
(或Elixir包装器库,在十六进制上找到gen_state_machine
)可以更轻松地完成您要实现的目标。您可以使用&#34;推迟&#34;模拟选择性接收。功能,它将把消息放回内部缓冲区,直到机器的状态发生变化,此时推迟的消息将按接收顺序处理。
还有其他一些好的技巧,比如能够生成&#34;内部&#34;在处理任何其他内容之前放置在邮箱头部的邮件。由于您的示例是一个非常明确的FSM案例,我建议您使用该路由,而不是在GenServer中重新创建它。