红回调和绿回调

有两种回调,我把他们分别叫做“红回调”和“绿回调”。红回调令人讨厌而且它打断你程序的控制流程。绿回调令人愉悦而且它不会打断你程序的控制流程。Javascript的回调是红回调而Erlang的回调是绿回调。

为了解释这个问题,我必须先回过头来讲讲I/O。

Erlang的并发I/O

Erlang是如何处理并发I/O的?相当简单!假设我们有三个进程A、B和C,它们在并行执行。我用如下的方式描述这个场景:

1
A || B || C

上述描述假设三个进程A、B、C正在进行I/O操作。

进程A如下所描述做一些事情:

1
A: --- read --- read --- write --- read --- write ---

进程B则如下描述:

1
B: ---- write --- write --- read --- read --- write ---

C进程也大致如此。虚线表示一些顺序的计算。进程A的Erlang代码框架如下:

1
2
3
4
5
6
7
8
a() ->
...
X = read(),
...
Y = read(),
...
write(...),
...

进程B和进程C的代码也和这个差不多。

Erlang里实际上是没有read()这个函数的。Erlang有 select receive 模式来处理消息,所以我们实际上定义的read()函数像如下所示:

1
2
3
4
5
6
7
read() ->
receive
Pattern1 ->
...
Pattern2 ->
...
end.

select receive 模式如何工作的细节不是本次讨论的重点,因此本文我将忽略这些细节。

重点是我在进程A的代码里如下所写:

1
2
3
...
X = read()
...

那么,我们的进程将在read函数处被挂起(或者叫做阻塞)直到read函数执行完毕。因此我们的代码“看起来像”是正在做一个同步阻塞的读动作。

看起来像是用双引号括起来的,因为它不是一个真正的阻塞读,而是一个真正的异步读,这个读操作不阻塞其他任何Erlang进程。

这真是棒极了,因为从一开始,程序就等待读请求完成,然后获得读取到的数据,程序继续运行。

理解并发

Erlang的I/O非常特别。当我们有两个并行的进程A和B,A进程里的一个读请求将明显地阻塞A进程,但是不会对其他任何并行的进程(这里指B)有影响。

所以A和B可以都同时执行写入操作就好像它们是连续的进程。

现在假设我们没有一个合适的基础并发模型。假设所有我们要做的事情都放入一个单独线程来执行。假设我们做一个读操作(它是阻塞的)而其他事情都在等待。啊!亲!我们的编程模型是更加清晰简单了,但是我们却浪费了CPU的宝贵资源。

现在在一些语言(正是我在研究的Javascript)没有多进程和多线程。不确切地说,它有一个线程,所有的事情都揉进这个线程里。在Javascript里要写与读相关操作的代码,你不得不用红回调,并且发明你自己的并发概念。

红回调

在Javascript里你肯定不想在主线程里做一个阻塞同步读操作(记住它只有一个线程),那么你必需设置一个回调,当读操作完成的时候触发这个回调。我叫这样的回调为红回调。你写的代码如下:

1
2
3
4
var done = function(x){ ... do something with x ..};
var error = function(x){ .... x …}
read(Something, {onSuccess:done, onError:error});
... ... more code ...

这样的代码搞得我脑袋一团浆糊。

当程序正在上述代码 more code 的某个地方执行的时候,读操作完成了,则必需立即回到done这个函数来执行,然后再回到前面 more code 中断的地方。我发现这个方式非常难以理解。

这的确很糟糕,每一个要解决并发问题的Javascript程序员必需要发明他自己的并发模型。问题就在于他们不知道他们正在做的是什么。每次一个Javascript程序员写下一行代码,说是“此处就该这么做”的时候,他实际上是在发明一个新的并发模型,并且在这些代码执行的时候他是没有任何线索知道这些代码是怎样交织在一起的。

(其实我对Javascript又爱又恨,它的大部分我都喜欢,但是就是恨它的并发模型。不过好笑的是,Javascript是没有并发模型的,所以对它也无从可恨了。:-)

更加难以理解的是错误。在共享内存的多线程回调代码里的错误更加是令我极度头痛!

绿回调

只是为了使得生活更加困惑,在Erlang里我们大量使用了回调。我把这些回调称为“绿回调”。因此回调并不一定是不好的。在Erlang里,我们可以在一个进程的上下文里更清晰地看到回调的执行,所以我们没有如何查看回调执行过程的问题。

如下是Erlang里一个绿回调的例子:

1
2
3
4
5
6
7
8
loop(F) ->
receive
{new_callback, F1} ->
loop(F1);
Msg ->
F(Msg),
loop(F)
end.

当一个进程运行这段代码的时候收到一个消息Msg,它就执行函数F(Msg)。这里没有任何不确定的,当回调被触发的时候我们确切地知道。在收到消息Msg后,这个回调被立即触发。

这一小段代码却非常漂亮。如果你给进程发送一个消息{new_callbak, F1},那么它将改变它的行为,在下一次调用的时候,它将执行新的回调。

我不知道你在Javascript里如何写出这样的代码。我写过大量的JQurey代码并且明白如何设置和删除回调。但是在删除一个事件处理并新增一个事件处理这期间里,这个事件被触发了,这会发生什么?我不知道。生命如此短暂如何花的起那么多时间来找这个答案。

Erlang的I/O是如何工作的

我们实际上并没有给进程发送消息。我们给进程的邮箱发送消息。每一个进程有一个邮箱,当我们给一个进程发送消息,这个消息被放入这个进程的邮箱(如果快递小子能找到这个进程的话)。

想像一下,Erlang的进程是有邮箱的房子。发送消息就好比你把你的消息给快递小子。快递小子的工作就是做两件事:把邮件放入目的邮箱并敲门说:“有新信到了”。

进程可能在忙着也可能在睡觉,就如一个房东一个样,他可能在干这活或者在睡觉。如果他在睡觉,那么当邮递员来了并敲响了房门,房东就会走到邮箱那里检查是否有令人感兴趣的邮件。

如果房东此时把手头的活干完后,正在做其他事情的时候,房东可能会走到邮箱那里检查是否有新的邮件到了,

这就是Erlang的消息工作机制。每一个房子(进程)有它自己的生命周期。邮递员投递邮件,房东根据自己的意愿决定什么时候去检查邮箱。

原文链接: http://joearms.github.io/2013/04/02/Red-and-Green-Callbacks.html