Erlang进程中的无限循环
Infinite loop in Erlang process
我是 Erlang
的新手,我试图实现一个简单的 class
,它具有一些模拟数据库的方法。 insert()
只是在流程图中插入一个 key -> value
,而 retrieve()
只是 returns 来自地图的值。但是,我陷入了 loop()
。我做错了什么?
-module(db).
-export([start/0,stop/0,retrieve/1,insert/2]).
start() ->
register(db, spawn(fun() ->
loop()
end)
),
{started}.
insert(Key, Value) ->
rpc({insert, Key, Value}).
retrieve(Key) ->
rpc({retrieve, Key}).
stop() ->
rpc({stop}).
rpc(Request) ->
db ! {self(), Request},
receive
{db, Reply} ->
Reply
end.
loop() ->
receive
{rpc, {insert, Key, Value}} ->
put(Key, Value),
rpc ! {db, done},
loop();
{rpc, {retrieve, Key}} ->
Val = get(Key),
rpc ! {db, Val},
loop();
{rpc, {stop}} ->
exit(db,ok),
rpc ! {db, stopped}
end.
所以,编译后:
我先打电话给db:start().
然后当尝试 db:insert("A", 1).
时,它卡住了。
谢谢
问题出在 loop/0
函数中。您正在使用 rpc
原子来模式匹配收到的消息 ({rpc, {insert, Key, Value}}
),但是,正如您在 rpc/1
函数中看到的那样,您总是发送格式为 {self(), Request}
的消息到数据库进程。
self()
函数 return 是格式为 <X.Y.Z>
的 PID,它永远不会与原子 rpc
匹配
例如,假设您尝试使用函数 insert/2
插入一些数据,而 self()
将 return PID <0.36.0>
。当rpc/1
发送消息时,在db ! {self(), {insert, Key, Value}}
行,loop/0
会收到{<0.36.0>, {insert, Key, Value}}
消息,永远不会匹配到{rpc, {insert, Key, Value}}
,因为rpc
是一个原子。
解决办法是把rpc
原子改成一个变量,像这样:
loop() ->
receive
{Rpc, {insert, Key, Value}} ->
put(Key, Value),
Rpc ! {db, done},
loop();
{Rpc, {retrieve, Key}} ->
Val = get(Key),
Rpc ! {db, Val},
loop();
{Rpc, {stop}} ->
Rpc ! {db, stopped},
exit(whereis(db),ok)
end.
Erlang 变量以大写字母开头,这就是我使用 Rpc
而不是 rpc
.
的原因
P.S.: 实际上,你还有另外两个问题:
- 在
loop/0
的最后部分,您处理 stop
消息的地方,您在实际回答 rpc
之前调用 exit(db, ok)
。在那种情况下,您将永远不会收到从 db
进程返回的 {db, stopped}
消息,到那时它已经死了。这就是我更改顺序的原因,将 exit/2
调用放在 Rpc ! {db, stopped}
. 之后
- 当您调用
exit/2
时,您传递的是 db
,它是一个原子,作为第一个参数,但是 exit/2
函数需要一个 PID 作为第一个参数,这会引发badarg
错误。这就是我将其更改为 exit(whereis(db), ok)
. 的原因
让我们更仔细地了解一下。 "rpc" 是什么意思? "Remote Procedure Call" -- 当然。但是 Erlang 中的 everything 都是一个 rpc,因此我们倾向于不使用该术语。相反,我们区分同步消息(其中调用者阻塞,等待响应)和异步消息(其中调用者只是触发消息并在世界上毫不关心地跑掉)。我们倾向于使用术语 "call" 表示同步消息,使用 "cast" 表示异步消息。
我们可以很容易地编写它,因为调用看起来很像上面的 rpc,在 Erlang 中添加了一个习惯用法,即添加一个唯一的引用值来标记消息并监控我们向其发送消息的进程以防万一它崩溃了(所以我们不会挂起,等待永远不会到来的响应......我们稍后会在您的代码中触及):
% Synchronous handler
call(Proc, Request) ->
Ref = monitor(process, Proc),
Proc ! {self(), Ref, Request},
receive
{Ref, Res} ->
demonitor(Ref, [flush]),
Res;
{'DOWN', Ref, process, Proc, Reason} ->
{fail, Reason}
after 1000 ->
demonitor(Ref, [flush]),
{fail, timeout}
end.
演员表更容易一些:
cast(Proc, Message) ->
Proc ! Message,
ok.
上面call的定义意味着我们发送给的进程会收到{SenderPID, Reference, Message}
形式的消息。请注意,这 不同于 与 {sender, reference, message}
,因为小写值是 atoms,这意味着它们是它们自己的值。
当我们 receive
消息时,我们在接收到的消息的形状和值上 匹配 。这意味着如果我有
receive
{number, X} ->
do_stuff(X)
end
在我的代码和 receive
中的进程收到一条消息 {blah, 25}
它 将不匹配 。如果它收到 另一个 消息 {number, 26}
那么它将匹配,receive
将调用 do_stuff/1
并且该过程将继续。 (这两件事——atoms
和 Variables
之间的区别以及 receive
中的匹配方式——是你的代码挂起的原因。)最初的消息,{blah, 25}
仍然会在邮箱中,但是,在队列的前面,所以下一个 receive
有机会匹配它。这个 属性 邮箱有时非常有用。
但是包罗万象是什么样子的呢?
以上你期待三种消息:
{insert, Key, Value}
{retrieve, Key}
stop
你给他们打扮得不一样,但这就是你想要做的事情的商业目的。 运行 通过我在上面编写的 call/2
函数插入的消息最终看起来像这样:{From, Ref, {insert, Key, Value}}
。因此,如果我们期望来自进程接收循环的任何响应,我们将需要匹配那个确切的形式。我们如何捕获意外消息或格式错误的消息?在 receive
子句的末尾,我们可以放置一个裸变量来匹配 anything else:
loop(State) ->
receive
{From, Ref, {insert, Key, Value}} ->
NewState = insert(Key, Value, State),
From ! {Ref, ok},
loop(NewState);
{From, Ref, {retrieve, Key}} ->
Value = retrieve(Key, State),
From ! {Ref, {ok, Value}},
loop(State);
{From, Ref, stop} ->
ok = io:format("~tp: ~tp told me to stop!~n", [self(), From]),
From ! {Ref, shutting_down},
exit(normal)
Unexpected ->
ok = io:format("~tp: Received unexpected message: ~tp~n",
[self(), Unexpected]),
loop(State)
end.
您会注意到我没有使用进程字典。不要使用进程字典。这不是它的目的。你会覆盖一些重要的东西。或者丢下重要的东西。或者...... bleh,只是不要这样做。使用 dict 或 map 或 gb_tree 或其他任何东西,并将其作为进程的 State
变量传递。一旦您稍后开始编写 OTP 代码,这对您来说将变得很自然。
稍微玩弄一下这些东西,您很快就会愉快地向您的进程发送垃圾邮件。
我是 Erlang
的新手,我试图实现一个简单的 class
,它具有一些模拟数据库的方法。 insert()
只是在流程图中插入一个 key -> value
,而 retrieve()
只是 returns 来自地图的值。但是,我陷入了 loop()
。我做错了什么?
-module(db).
-export([start/0,stop/0,retrieve/1,insert/2]).
start() ->
register(db, spawn(fun() ->
loop()
end)
),
{started}.
insert(Key, Value) ->
rpc({insert, Key, Value}).
retrieve(Key) ->
rpc({retrieve, Key}).
stop() ->
rpc({stop}).
rpc(Request) ->
db ! {self(), Request},
receive
{db, Reply} ->
Reply
end.
loop() ->
receive
{rpc, {insert, Key, Value}} ->
put(Key, Value),
rpc ! {db, done},
loop();
{rpc, {retrieve, Key}} ->
Val = get(Key),
rpc ! {db, Val},
loop();
{rpc, {stop}} ->
exit(db,ok),
rpc ! {db, stopped}
end.
所以,编译后:
我先打电话给db:start().
然后当尝试 db:insert("A", 1).
时,它卡住了。
谢谢
问题出在 loop/0
函数中。您正在使用 rpc
原子来模式匹配收到的消息 ({rpc, {insert, Key, Value}}
),但是,正如您在 rpc/1
函数中看到的那样,您总是发送格式为 {self(), Request}
的消息到数据库进程。
self()
函数 return 是格式为 <X.Y.Z>
的 PID,它永远不会与原子 rpc
例如,假设您尝试使用函数 insert/2
插入一些数据,而 self()
将 return PID <0.36.0>
。当rpc/1
发送消息时,在db ! {self(), {insert, Key, Value}}
行,loop/0
会收到{<0.36.0>, {insert, Key, Value}}
消息,永远不会匹配到{rpc, {insert, Key, Value}}
,因为rpc
是一个原子。
解决办法是把rpc
原子改成一个变量,像这样:
loop() ->
receive
{Rpc, {insert, Key, Value}} ->
put(Key, Value),
Rpc ! {db, done},
loop();
{Rpc, {retrieve, Key}} ->
Val = get(Key),
Rpc ! {db, Val},
loop();
{Rpc, {stop}} ->
Rpc ! {db, stopped},
exit(whereis(db),ok)
end.
Erlang 变量以大写字母开头,这就是我使用 Rpc
而不是 rpc
.
P.S.: 实际上,你还有另外两个问题:
- 在
loop/0
的最后部分,您处理stop
消息的地方,您在实际回答rpc
之前调用exit(db, ok)
。在那种情况下,您将永远不会收到从db
进程返回的{db, stopped}
消息,到那时它已经死了。这就是我更改顺序的原因,将exit/2
调用放在Rpc ! {db, stopped}
. 之后
- 当您调用
exit/2
时,您传递的是db
,它是一个原子,作为第一个参数,但是exit/2
函数需要一个 PID 作为第一个参数,这会引发badarg
错误。这就是我将其更改为exit(whereis(db), ok)
. 的原因
让我们更仔细地了解一下。 "rpc" 是什么意思? "Remote Procedure Call" -- 当然。但是 Erlang 中的 everything 都是一个 rpc,因此我们倾向于不使用该术语。相反,我们区分同步消息(其中调用者阻塞,等待响应)和异步消息(其中调用者只是触发消息并在世界上毫不关心地跑掉)。我们倾向于使用术语 "call" 表示同步消息,使用 "cast" 表示异步消息。
我们可以很容易地编写它,因为调用看起来很像上面的 rpc,在 Erlang 中添加了一个习惯用法,即添加一个唯一的引用值来标记消息并监控我们向其发送消息的进程以防万一它崩溃了(所以我们不会挂起,等待永远不会到来的响应......我们稍后会在您的代码中触及):
% Synchronous handler
call(Proc, Request) ->
Ref = monitor(process, Proc),
Proc ! {self(), Ref, Request},
receive
{Ref, Res} ->
demonitor(Ref, [flush]),
Res;
{'DOWN', Ref, process, Proc, Reason} ->
{fail, Reason}
after 1000 ->
demonitor(Ref, [flush]),
{fail, timeout}
end.
演员表更容易一些:
cast(Proc, Message) ->
Proc ! Message,
ok.
上面call的定义意味着我们发送给的进程会收到{SenderPID, Reference, Message}
形式的消息。请注意,这 不同于 与 {sender, reference, message}
,因为小写值是 atoms,这意味着它们是它们自己的值。
当我们 receive
消息时,我们在接收到的消息的形状和值上 匹配 。这意味着如果我有
receive
{number, X} ->
do_stuff(X)
end
在我的代码和 receive
中的进程收到一条消息 {blah, 25}
它 将不匹配 。如果它收到 另一个 消息 {number, 26}
那么它将匹配,receive
将调用 do_stuff/1
并且该过程将继续。 (这两件事——atoms
和 Variables
之间的区别以及 receive
中的匹配方式——是你的代码挂起的原因。)最初的消息,{blah, 25}
仍然会在邮箱中,但是,在队列的前面,所以下一个 receive
有机会匹配它。这个 属性 邮箱有时非常有用。
但是包罗万象是什么样子的呢?
以上你期待三种消息:
{insert, Key, Value}
{retrieve, Key}
stop
你给他们打扮得不一样,但这就是你想要做的事情的商业目的。 运行 通过我在上面编写的 call/2
函数插入的消息最终看起来像这样:{From, Ref, {insert, Key, Value}}
。因此,如果我们期望来自进程接收循环的任何响应,我们将需要匹配那个确切的形式。我们如何捕获意外消息或格式错误的消息?在 receive
子句的末尾,我们可以放置一个裸变量来匹配 anything else:
loop(State) ->
receive
{From, Ref, {insert, Key, Value}} ->
NewState = insert(Key, Value, State),
From ! {Ref, ok},
loop(NewState);
{From, Ref, {retrieve, Key}} ->
Value = retrieve(Key, State),
From ! {Ref, {ok, Value}},
loop(State);
{From, Ref, stop} ->
ok = io:format("~tp: ~tp told me to stop!~n", [self(), From]),
From ! {Ref, shutting_down},
exit(normal)
Unexpected ->
ok = io:format("~tp: Received unexpected message: ~tp~n",
[self(), Unexpected]),
loop(State)
end.
您会注意到我没有使用进程字典。不要使用进程字典。这不是它的目的。你会覆盖一些重要的东西。或者丢下重要的东西。或者...... bleh,只是不要这样做。使用 dict 或 map 或 gb_tree 或其他任何东西,并将其作为进程的 State
变量传递。一旦您稍后开始编写 OTP 代码,这对您来说将变得很自然。
稍微玩弄一下这些东西,您很快就会愉快地向您的进程发送垃圾邮件。