耗尽弹药时你不能爆炸!

本文(和上一篇文章一样)也是受我在哥伦比亚卡利的学生的好奇心所启发。这次的问题是:什么时候并且为什么 bang(!)(即消息发送)操作符失败?

有时候你有无限的子弹

那些天我和我的学生讨论的是Erlang的一个核心概念:消息传递。Erlang中消息传递是用发送操作符(即 !,也叫做bang)来完成的。这次我给他们介绍bang,我用一个Pid放在它的左边。就像这样:

1
2
3
4
5
1> Pid = self().
<0.65.0>
2> Pid ! {a, message}.
{a,message}
3>

在这种情况下(当表达式的左边是一个Pid),bang从来不失败。即使Pid表示的是一个死亡进程。

1
2
3
4
5
6
7
8
9
3> 1 / 0.
** exception error: an error occurred when evaluating an arithmetic expression
in operator '/'/2
called as 1 / 0
4> erlang:is_process_alive(Pid).
false
5> Pid ! {another, message}.
{another,message}
6>

有时候你就是不能开枪

但是Pid并不是在bang左边唯一有效的表达式,你也可以用原子和元组放在bang的左边,例如:

1
2
3
4
5
6
7
8
9
10
11
6> register(my, self()).
true
7> my ! {third, message}.
{third,message}
8> {my, node()} ! {fourth, message}.
{fourth,message}
9> flush().
Shell got {third,message}
Shell got {fourth,message}
ok
10>

在这种情况下,如果没有进程被注册为这个名字,bang操作可能失败:

1
2
3
4
5
10> no_proc ! {fifth, message}.
** exception error: bad argument
in operator !/2
called as no_proc ! {fifth,message}
11>

去读官方文档吧!

所有问题在官方文档里都有清晰的说明。

Expr1的计算结果必须是一个pid、一个注册的名字(原子),或者是一个格式为{Name, Node}的元组。Name是一个原子而Node是一个节点名字,也是一个原子。

如果Expr1的计算结果是一个名字,但是这个名字没有注册,则一个badarg运行时错误发生。

发送消息给一个pid永远都不会失败,即使这个pid指的是一个不存在的进程。

也就是说,如果Expr1的计算结果是一个格式为{Name, Node}元组(或者是另外一个节点的pid),分布式消息发送也永远不会失败。

是的,不过…

现在,我们不能只是满足了解这点知识。

正如你可能猜到的,我们没有那样做。我们问自己:

当没有进程用相关名字注册,而如果发送消息给这个名字时,会抛出badarg错误:
1、一个进程可以用一个名字注册,但它却是不存在的?换句话说:bang操作失败意味着这个进程不存在?
2、我如何在用bang操作前检查是否有进程用相关名字注册。

问题1:我能注册已经死亡的进程吗?

为回答问题1,我们必须先检查一些事情。一个进程可以死掉并且还能用一个名字注册吗?我们用两种方法来尝试:

1
2
3
4
5
6
7
8
9
10
11
1> Pid = self().
<0.65.0>
2> 1 / 0.
** exception error: an error occurred when evaluating an arithmetic expression
in operator '/'/2
called as 1 / 0
3> register(my, Pid).
** exception error: bad argument
in function register/2
called as register(my,<0.65.0>)
4>

首先我们尝试一个已经死亡的进程作为入参来调用erlang:register/2函数,结果我们失败了。然后我们尝试注册一个进程,接着杀掉它:

1
2
3
4
5
6
7
8
9
10
11
4> register(my, self()).
true
5> 1 / 0.
** exception error: an error occurred when evaluating an arithmetic expression
in operator '/'/2
called as 1 / 0
6> my ! message.
** exception error: bad argument
in operator !/2
called as my ! message
7>

还是失败了。不过这次的失败是因为进程已经死亡了还是因为发送消息的时候它没有被注册?这就带我们进入问题2…

问题2:我怎么能够阻止bagarg错误发生?

现在,我如何可以找出实际在对某个名字使用bang操作之前是否有一个用这个名字注册的进程?

快速而且非常精确的答案是:你做不到。因为Erlang的并发本质,在你检测进程是否存在的时候和你发送消息的时候这两个时间点之间,这个进程可能已经死掉了(或者一个新的进程使用了相同的名字来注册)。

尽管如此,还是有一个方法来只是检查在你的节点上是否有一个用某个名字注册的进程,并找到那个进程是哪个进程来作为奖赏:whereis/1

1
2
3
4
5
6
7
8
9
10
11
12
13
7> whereis(my).
undefined
8> register(my, self()).
true
9> whereis(my).
<0.75.0>
10> 1 / 0.
** exception error: an error occurred when evaluating an arithmetic expression
in operator '/'/2
called as 1 / 0
11> whereis(my).
undefined
12>

如果whereis/1返回undefined,你就知道在你的节点里没有用那个名字注册的进程。但是,如上所述,您不应该编写像如下的代码:

1
2
3
4
case whereis(my) of
undefined -> do_nothing;
APid -> APid ! message
end

你那是正在给自己购买了去竞争条件之地的票。你所需要的就如下代码所示:

1
2
3
4
try my ! message
catch
_:badarg -> do_nothing
end

但是我们讨厌 try…catch!

如果你真的不喜欢像上面那样用try…catch来处理每一个消息,这里有一个小技巧。还记得前面官方文档说过的吗?

也就是说,如果Expr1的计算结果是一个格式为{Name, Node}元组(或者是另外一个节点的pid),分布式消息发送也永远不会失败。

考虑到这一点,看看你能做些什么来替代上面的做法:

1
2
3
4
5
6
7
12> my ! message.
** exception error: bad argument
in operator !/2
called as my ! message
13> {my, node()} ! message.
message
14>

使用元组语法,其中用到了你自己所在节点的名称,如果那个叫做my的进程存在的话,你就正在有效地发送消息给它;而如果这个进程不存在,则会丢弃这些消息。如果你用Pid来替换元组的话,也是这个结果。

附加内容

我想用我们在卡利发现的另一个比较酷的事情来结束本文。当谈论这些事情的时候,我们并不真正地了解bang操作在Erlang虚拟机内部的运作过程,只是考虑bang操作失败的情况,我们尝试想像Erlang虚拟机内部如何运行。

可能的错误

首先来考虑这个问题:当bang操作失败的时候,它总是报badarg错误。当你用一个没有注册为进程名字的原子的时候,它会失败,但是你用其他东西,它也会失败。看如下例子:

1
2
3
4
5
6
7
8
9
1> x ! message.
** exception error: bad argument
in operator !/2
called as x ! message
2> "a list" ! message.
** exception error: bad argument
in operator !/2
called as "a list" ! message
3>

用一点想象力,我们可能会认为,当你使用一个原子,Erlang虚拟机转换原子为Pid,然后在其上执行bang操作的原始版。如果它未能把原子转换成Pid,那么当然badarg错误就发生。换句话说,看起来Erlang VM在将下面的语句…

1
1> x ! message.

改为如下的语句:

1
1> whereis(x) ! message.

那么,当whereis 返回 undefined, bang操作失败。

等一等

哦,不错。但是undefined也是一个原子啊。Erlang虚拟机为何在那里不陷入一个无限的递归循环呢?嗯,它可能有一个处理undefined的特殊分支。在这个分支里,它可以避免把undefined当做其他任何原子来处理。但是,那么,如果我注册一个进程的名字为undefined呢?

让我们来试试,好吗?

1
2
3
4
5
3> register(undefined, self()).
** exception error: bad argument
in function register/2
called as register(undefined,<0.69.0>)
4>

好吧,我们做不到。实际上官方文档说的很清楚。

badarg
如果 RegName 是原子undefined.

干得不错,Erlang/OTP团队。干得不错,真的!

原文链接: https://medium.com/erlang-battleground/running-out-of-ammo-769bb28baac2#.sm8fbhd9q