首先,我们聊聊现实世界中的并发。
我曾经举过一个并发和并行的例子:
老妈在很短的时间给我安排了很多任务,吃包子,拖地,洗碗…等等
由于我母亲大人比较严厉,所以我“愉快”地接受了所有任务。
这就是所谓的并发,在某一时段,能接受的事件。
我只有两只手,可以用一只手来吃包子,另一只手来拖地。
这就是所谓的并行,在某一时刻,能处理的事件。
现实世界中,并发的事情无处不在,就拿上面的例子来说,我答应了老妈的多个要求,老妈也收到了我的回复。这说明我们的大脑天生就是并发的,它可以在某一时段接受大量的信息。
所以,符合我们思维的并发应该如上图,我可以接收老妈的多个消息,即使老爸回来给我发消息,我也能接收。
这是现实世界中的并发,人与人之间是单独的个体,通过发送消息进行交流。
此时,你可能已经猜到这就是Erlang 中的并发,Actor模型的样子。
让我们先跨过这个,来看看传统的并发是如何做的。
共享内存
在我们平时写的程序中,并发事件处理一般是通过多线程或多进程来处理。
某一时刻,我们同时接收到了多条请求,通常的做法是将它放入队列(共用的内存)中去,每一个线程或者进程都会去队列中取消息进行处理。
如下图:
此时,队列中的内存是共享的,多个线程或进程存取会造成竟态条件,也就是产生竞争,发生错误。
通常的做法是 加锁。
线程或进程必须先抢到锁,接着抢到锁的才能访问队列中的消息。
注意,锁是错误的源泉。
我们应该都遇到过死锁等错误,先不说性能,调试起来就很麻烦。
那么 无锁 CAS 呢?
每个线程或进程都先取出一条消息,保存旧值,拷贝一份后修改为新值,将自己保存的旧值和原先队列中的值比较,若相同说明没有被其它线程或进程修改,则这条消息属于该线程或进程,可以处理此消息。
若旧值和新值不同,说明有其它线程或进程得到了此消息,则循环进行下一次 CAS 操作。
这就是所谓的copy and set。
这里我们不对比这两种方式以及一会说的 Actor 模型性能的好坏。
因为在不同的场景下,不同的方式性能也是不同的。我们需要根据具体情况,测试,分析,从而得出性能最优的方式。顺便提一句,Actor只是模型,仅仅表现给我们的是消息传递。至于它内部怎样实现这里不讨论,感兴趣可以看看内部实现
不过从刚才的描述来看,上述方式都不符合我们的思维,而且略复杂,相信没有人是通过 共享大脑 来传递及处理消息的吧?
来看看 Actor 模型
Actor 模型概念非常简单,且非常符合我们的思维。
万物皆为 Actor,Actor 与 Actor 之间通过发送消息来通信。
就和人类一般,一个人是一个 Actor,人与人之间通过消息来交互。
别惊讶,Actor 模型就是这么简单。(Actor 模型更多细节参见 Wiki Actor)
接着我们来看 Erlang 是如何运用 Actor 模型的。
Erlang 的 Actor 模型也非常简单。
在 Erlang 中,进程为最小的单位,也就是所谓的 Actor。注意 Erlang 的进程不是我们传统上的进程,它运行在 Erlang 虚拟机上,非常小,非常轻,可以瞬间创建上万,甚至几十万个,进程间完全是独立的,不共享内存。在进程运行时若出现错误,由于进程的轻量级,Erlang 采取的措施是“让其他进程修复”和“任其崩溃”。在 Erlang 上查看默认限制数量是26万多,可以进行修改。每个进程创建后都会有一个独一无二的 Pid,这些进程之间通过 Pid 来互相发送消息,进程的唯一交互方式也是消息传递,消息也许能被对方收到,也许不能,收到后可以处理该消息。如果想知道某个消息是否被进程收到,必须向该进程发送一个消息并等待回复。
Erlang 中的并发编程只需要如下几个简单的函数。
Pid = spawn(Mod,Func, Args)
创建一个新的并发进程来执行Mod模块中的 Fun(),Args 是参数。
Pid ! Message
想序号为 Pid 的进程发送消息。消息发送是异步的,发送方不等待而是继续之前的工作。
receive… end
接受发送给某个进程的消息,匹配后处理。
receive
Pattern 1 [when Guard1] ->
Expression1;
Pattern 2 [when Guard2] ->
Expression2;
...
after T ->
ExpressionTimeout
某个消息到达后,会先与 Pattern 进行匹配,匹配相同后执行,若未匹配成功消息则会保存起来待以后处理,进程会开始下一轮操作,若等待超时 T,则会执行表达式 ExpressionTimeout。
举个例子:
现在我们要进行两个进程的消息传递,一个进程发送Num1 和 Num2以及对应的操作标识,另外一个进程接受到消息后计算。
比如 {plus, Num1, Num2} 就是求 Num1 和 Num2 的和。
-module(calculate).
-export([loop/0, start/0]).
% 创建新进程,并执行 loop 函数。
start() -> spawn(calculate, loop, []).
% loop 函数
loop() ->
receive % 接受消息并进行匹配
{plus, Num1, Num2} -> % 匹配加法
io:format("Num1 plus Num2 result:~p~n", [Num1 + Num2]),
loop();
{reduce, Num1, Num2} -> % 匹配减法
io:format("Num1 reduce Num2 result:~p~n", [Num1 - Num2]),
loop();
{multi, Num1, Num2} -> % 匹配乘法
io:format("Num1 multi Num2 result:~p~n", [Num1 * Num2]),
loop();
{divis, Num1, Num2} -> % 匹配除法
io:format("Num1 divis Num2 result:~p~n", [Num1 div Num2]),
loop()
after 50000 -> % 超时操作
io:format("timeout!!")
end().
执行结果:
我们一行一行来看。
第一行(标号为 1>),编译 calculate.erl。
第二行,执行 calculate 模块中的 start 函数并且创建进程,创建后将进程的 Pid 赋值给 Pid1。
第三行,打印 Pid1。
第四行,创建 Pid2 的进程并且向 Pid1 进程发送请求消息,计算 Num1 和 Num2 的和,Pid1 进程计算完毕后并打印。
第五行,计算减法并打印。
第六行,报错因为 Pid3 已经被使用,在 Erlang 中变量赋值一次后就不能被改变了,不会变就不会出现多个进程修改导致不一致问题。
第七行,计算乘法并打印结果。
第八行,计算除法并打印结果。
最后,超时发生错误。
由此可见 Erlang 并发编程也非常简单且符合人们的思维。
这篇文章就简单地介绍到这里,希望有所帮助^_^。
完