erlang 并发编程!
回顾并发
首先需要解释两个概念,并发与并行:
并发:同一时间段内可以交替处理多个操作,强调同一时段内交替发生。
并行:同一时刻内同时处理多个操作,强调同一时刻点同时发生
如果我想要在C++中使用并发我应该怎么做呢——std::thread(如果笔者没记错的话,在C++11引入之前,多线程编程甚至需要靠编译平台提供额外的API才能实现)
如果在Java中则需要继承Thread类并实现Runnable接口
同时因为以上两种模型的资源都是在进程的各个线程中共享的,不可避免就出现了资源安全问题,以此带来了锁和钥匙的问题,对资源的访问就需要加锁和解锁的问题(比如java中stringbuffer和stringbuilder的区别)
而在erlang语言中,则不存在这种问题,因为erlang中,没有钥匙,更没有锁。
如何并发
Erlang的并发是基于进程(process)的。进程是一些独立的小型虚拟机,他们有自己的内存(意味着数据的安全性,意味着不再需要锁和钥匙),可以执行Erlang函数。在Erlang中,process不在隶属于操作系统,而是编程语言(这在其他顺序型编程语言中显得不可思议)这就意味着Erlang的进程在任何操作系统上都会具有相同的逻辑行为,这样,就能编写可移植的并发代码,让它在任何支持Erlang的操作系统上运行。
基本功能函数
基于erlang语言的特性,并发编程便变得十分简单,只需要三个函数,他们是spawn、send和receive。
spawn
如其他语言一样,你需要一个新的进程时自然需要去创建它,这个级别函数便是帮助你创建一个新的进程(就像在linux里fork了一下,一个崭新的进程便创建成功了)
Pid = spawn(Mod,Func,Args) %% Pid 此函数的返回值 进程标识符!
%% 使用此方法时切记 Mod中的Func 需要export
Pid = spawn(Fun) %% 传入匿名函数 该函数不需要导出
以上是spawn的两种使用方式,但是注意 得益于erlang 的热升级机制
请多使用MFA(第一种)方式,如果你需要升级你的代码的话
因为Func总是会调用最新的代码版本
send
erlang中的send使用的是!操作符,称之为发送操作符,使用方式如下
Pid!Message %% 此操作是异步的
如果你需要发送给多个进程 你可以这样操作:
Pid1!Pid2!Pid3!Message
receive
接收则需要进程使用邮箱(在进程创建时会同步创建),通过receive来读取邮箱中的信息
receive
Message1 when WhatHappen ->
Express1;
Message2 ->
Express2
end
并发编程实例
-module(study).
-export([get_result/0]).
%%-compile(export_all).
%% area({r,W,H}) ->
%% W*H.
%% area({s,S})->
%% S*S.
get_result() ->
receive
{r,W,H} ->
io:format("~p~n",[W*H]);
{s,S} ->
io:format("~p~n",[S*S])
end.
export_all标签会将所有函数导出,仅开发测试环境可用,正式环境中禁止使用此flag。
至此 我们完成了进程的创建 以及进程之前的消息传递。
超时接受
超时接受是receive的拓展。有时我们期望在一定的时间内接收到某条消息(消息的时效性问题),如果我们迟迟没有收到某条消息怎么办呢?erlang为我们提供了after条件,
get_result() ->
receive
{r,W,H} ->
io:format("~p~n",[W*H]);
{s,S} ->
io:format("~p~n",[S*S])
after 5000 ->
io:format("Something Bad Happen!")
end.
可以看到五秒后我们不再等待信息,而是执行after中的express。
如果Time为0会怎样? 会导致after里的主体立即触发,但是在这之前,系统仍然会对邮箱里的消息进行一次模式匹配。
此外Time还可以设置为原子infinity,after则永远不会触发。
因此我们很容易想到使用after来制作定时器,在很多通信协议中,定时器和超时都是非常关键的东西,毕竟没有人希望无限期的等待下去。
注册进程
之前提到,想要与一个进程通信,自然需要这个进程的Pid,但是Pid是如何产生的?是我们在父进程中通过spawn产生的,也就意味着,只有父进程才知道这个新进程的Pid,这样对其他想要与新进程通信的进程是十分不便的,因为我们需要父进程告诉我他的子进程标识符我才能与之对话,为此erlang有一种公布进程标识符的方法,为进程注册(register)名称。
register(NameAtom,Pid)
功能如其单词含义,为Pid这个进程标识符的进程,以NameAtom(原子)为名称进行注册管理。如果这个NameAtom已经存在,则此处register会失败,但是一旦register成功,则可以通过NameAtom!message与该进程通信!
unregister(NameAtom)
有注册自然会有注销,我们希望放弃某个进程别名时,可以通过此方法注销它
需要注意的是,当这个注册过的进程崩溃时(这是常有的事),会自动取消注册
whereis(NameAtom)
当我们需要找某个进程时,自然便需要问一问这个进程是否存在
进程存在时返回进程标识符,不存在时返回undefined
registered()
那如果我们想知道目前系统中有多少注册进程怎么办呢,总不能一个一个去试吧。别担心,erlang提供了方法让我们知道目前有多少进程在线:
可以看到已经有很多erlang的系统进程在线。
并发中的错误
在顺序语言编程中,我们往往强调防御式编程,因为如果这个进程因为某些原因出行异常导致终止,这往往是非常麻烦的一件事(比如windows因为critical process died导致的蓝屏)。但在erlang中,创建进程的内存开销以及时间消耗其实是非常小的,因此我们更多的是采取措施检测错误,然后等待错误发生,并纠正它。
Erlang在关于构建容错式软件的理念可以总结成:让其他进程修复错误和任其崩溃。
link(连接)
既然我希望能让其他进程知道本进程发生了错误崩溃了,那本进程自然希望其他的进程能够检查本进程的健康状态,通过link我能让别人知道本进程的健康状况,在本进程发生崩溃时能告知其他进程,反之亦然。
需要注意的点是,相连接的进程会组成一个进程组,如果有其中一个进程发生了崩溃,其他相连进程会因为进程组里进程的崩溃而一起崩溃。
如果希望阻止进程崩溃的扩散需要通过使用 process_flag(trap_exit, true) 使指定的进程转变为系统进程来阻止崩溃的扩散。
上图演示了当进程中有崩溃出现时,连接的进程会扩散奔溃的情况
monitor(监视)
监视和连接很相似,但它是单向的。如果本进程A监视进程B,而B出于某种原因终止了,就会向A发送一个“宕机”消息,但反过来就不行了。因为A能通过检查B的健康状况在B崩溃时接手B的任务,完成计算任务。这种思想在非常多的软件上都有体现(比如Redis中的哨兵模式)。
上图演示了当子进程处于父进程的监视状态下,不会对其他进程造成连环崩溃影响。