Erlang 中的并发 -- Actor 模型

首先,我们聊聊现实世界中的并发。

我曾经举过一个并发和并行的例子:

老妈在很短的时间给我安排了很多任务,吃包子,拖地,洗碗…等等
由于我母亲大人比较严厉,所以我“愉快”地接受了所有任务。
这就是所谓的并发,在某一时段,能接受的事件
我只有两只手,可以用一只手来吃包子,另一只手来拖地。
这就是所谓的并行,在某一时刻,能处理的事件

现实世界中,并发的事情无处不在,就拿上面的例子来说,我答应了老妈的多个要求,老妈也收到了我的回复。这说明我们的大脑天生就是并发的,它可以在某一时段接受大量的信息。

所以,符合我们思维的并发应该如上图,我可以接收老妈的多个消息,即使老爸回来给我发消息,我也能接收。

这是现实世界中的并发,人与人之间是单独的个体,通过发送消息进行交流
此时,你可能已经猜到这就是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 并发编程也非常简单且符合人们的思维。

这篇文章就简单地介绍到这里,希望有所帮助^_^。


  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏天的技术博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值