假期7天学会Elixir,掌握函数式编程与 Actor 模型

开发 前端
Elixr/Erlang 天然支持分布式, 而作者 Joe Armstrong 在论文中是这样认为的:几乎所有传统的编程语言对真正的并发都缺乏有力的支持 —— 本质上是顺序化的,而语言的并发性都仅仅由底层操作系统而不是语言提供。

[[426805]]

为什么你一定需要学习 Elixir?

Elixir 是一门基于 erlang 开发的新语言,复用了 erlang 的虚拟机以及全部库(站在已经生存了20多年巨人的肩膀上),定义了全新的语法以及构造了现代语言必不可少生态环境—包管理器,测试工具,formatter等。使用 Elixir,你可以方便的构建可用性高达99.9999以及天然分布式的程序(代码随手一写就是稳定的分布式),可以秒开成千上万 Elixir 里专属的进程(比起系统的进程更轻量级),处理高并发请求等等。

Elixr/Erlang 天然支持分布式, 而作者 Joe Armstrong 在论文中是这样认为的:几乎所有传统的编程语言对真正的并发都缺乏有力的支持 —— 本质上是顺序化的,而语言的并发性都仅仅由底层操作系统而不是语言提供。而用对并发提供良好支持的语言(也就是作者说的面向并发的语言 Concurrency Oriented Language) 来边写程序,则是相当容易的:

  • 从真实世界的活动中识别出真正的并发活动
  • 识别出并发活动之间的所有消息通道
  • 写下能够在不同消息通道中流通的所有消息

Elixir 是怎么样的语言?

Elixir 是函数式语言,与 java,C++等过程式语言不通,没有变量。或者说,变量全都imutable(不可改变)。通过学习Elixir, 你可以学习多一种编程范式。

python 中你是这样子处理列表的:

  1. mylist = [] 
  2. mylist.append('Google'
  3. mylist.append('Facebook'
  4. print mylist #结果是['Google''Facebook'

Elixir 中是这样子的:

  1. myList = [] 
  2. myList = List.insert_at(myList, 0, "Google"
  3. myList = List.insert_at(myList, 1, "Facebook"
  4. IO.inspect myList 

elixir 中这不是正常的写法,不过我只是用这些例子来介绍异同点。

注意到,在面向对象思维的语言中,处理列表,是 用对象的方法,mylist.append: 对象.动作来处理;而函数式因为变量是不可变的,是要 List.append(mylist, xx), 对象模块.动作(哪个对象)来处理,同时会返回修改后的新对象。

数据不可变,好处就是在高并发中,并不会因为状态多且不断变化,引致debug 异常困难——本来人的大脑就不适应多线程。

不可变,就意味着 for 与 while循环用不了,因为不存在变量 不断 变化,达到某值就中止循环~因此,你只能用递归来实现 while。

但是不怕,Elixir 提供了强大无比的抽象, each 函数,map 函数,reduce 函数,all? 函数(判断列表所有值是否满足此条件),group 函数(类似数据库的 group) 等等,只有你想不到。相比之下,golang 真的是乏善可陈。

Go 语言的创始人之一,Unix 老牌黑客罗勃?派克(RobPike)的忠告:

所以说,没有泛型就是不行的。。。

管道

是的,类似 linux 的管道 |,把处理结果传递给下一个函数。

  1. 1..100 
  2. |> Enum.map(fn x-> x+1 end
  3. |> Enum.filter(fn x-> rem(x, 2)==0 end
  4. |> Enum.filter(fn x-> rem(x, 3)==0 end
  5. |> Enum.filter(fn x-> rem(x, 5)==0 end
  6. |> IO.inspect 

与以下的 代码相比,python是否相形见绌?

  1. numbers = range(1, 100) 
  2. numbers = map( (lambda x: x+1), numbers ) 
  3. numbers = filter( (lambda x: x%2 == 0), numbers ) 
  4. numbers = filter( (lambda x: x%3 == 0), numbers ) 
  5. numbers = filter( (lambda x: x%5 == 0), numbers ) 
  6. print(numbers) 

再来一个例子,来自Dave Long 的博客 Playing with Elixir Pipes[1] :

代码的作用是:取出请求的头部 x-twilio-signature 签名,并且校验是否有效。

没有管道时,代码是这样子的:

  1. signature = List.first(get_req_header(conn, "x-twilio-signature"))   
  2. is_valid = Validator.validate(url_from_conn(conn), conn.params, signature)   
  3. if is_valid do   
  4.   conn 
  5. else   
  6.   halt(send_resp(conn, 401, "Not authorized")) 
  7. end 

加上管道:

  1. signature = conn   
  2.             |> get_req_header("x-twilio-signature"
  3.             |> List.first 
  4. if conn   
  5.    |> url_from_conn 
  6.    |> Validator.validate(conn.params, signature) 
  7. do   
  8.   conn 
  9. else   
  10.   conn |> send_resp(401, "Not authorized") |> halt 
  11. end 

逻辑就非常清晰了。还可以这样子写:

  1. signature = conn   
  2.             |> get_req_header("x-twilio-signature"
  3.             |> List.first 
  4. conn   
  5. |> url_from_conn 
  6. |> Validator.validate(conn.params, signature) 
  7. |> if(do: conn, else: conn |> send_resp(401, "Not authorized") |> halt) 

进程 Actor Model

轻量级的进程

在 Elixir 里,Elixir进程(以下简称进程,与系统进程区分开)是轻量级的进程,与操作系统的概念相差不多,只不过 Elixir 进程运行在虚拟机中。那为什么 Elixir 进程更快呢?

  • Erlang 进程的堆栈是动态分配、随使用增长的,新建一个 Erlang 进程的开销远比系统进程 / 线程小得多,开销就像在 OO 语言中建立一个新对象般简单。
  • 普通进程 / 线程的内存管理是基于页的,而页对于一个函数 + 一点点零碎来说都太大了。而实际中 OS 分配给普通进程的初始栈可以达到 Megabytes 级别。
  • Erlang 进程之间是隔离的,没有共享状态,所有的消息都是异步的,不会继承大量的已有状态。
  • Erlang 进程的调度是在 Erlang VM 内发生的,跟 OS 层没啥关系,无需普通进程 / 线程切换时的各种开销
  • Erlang 进程的切换是一种类似直接 “跳转” 的方式,以 O(1) 复杂度实现。Erlang 调度器会管理这些切换,大概只需要几十个指令和数十纳秒的时间。普通线程的切换会需要数百上前纳秒,OS 调度器的运作复杂度可能是 O(logn) 或者 O(log(logn))。如果有上万个线程,这个时间将会大幅提升。来自知乎[2]

像指挥交响乐队一样,指挥你的 Elixir 进程

对于Elixir 进程,你可以方便的用一个进程(supervisor)去管理子进程,supervisor会根据你设定的策略,来处理意外挂掉的子进程(这种情况不多的是,错误处理稍微做不好就会挂) , 策略有:

  • one_for_one:只重启挂掉的子进程
  • one_for_all:有一个子进程挂了,重启所有子进程
  • rest_for_one:在该挂掉的子进程 创建时间之后创建的子进程都会重启。

老夫敲代码就是一把梭!可不,只要重启就行。

实质上,这是有论文支持的: 在复杂的产品系统中,几乎所有的故障和错误都是暂态的,对某个操作进行重试是一种不错地解决问题方法——Jim Gray的论文[3]中指出,使用这种方法处理暂态故障,系统的平均故障间隔时间(MTBF)提升了 4 倍。

因此,你就可以创建一课监控树,根节点就是啥事都不做,只负责监控的进程。其他都是它的子进程,如果不是 coredump(几乎不发生),那么根节点就不可能会挂;因此其他子进程就会正确的被处理。

当然,这有前提:5 秒内重启超于 3 次,就会不再重启,让进程挂掉。为什么呢?因为重启是为了让进程回到当初启动时的稳定态,既然稳定态都不稳定了,重复做重启是没有意义的,这时迫切需要人来处理。

方便的通信

一切皆消息。

进程间通信,就像微风一样自然。你所监管的进程而来的信息,调用的库的消息,全部都可以自己来 handle 并作相应处理。甚至还有抽象好的 GenServer 来让你专门处理消息与状态逻辑。

定时器?不需要的,我们甚至可以自己发送消息来实现更好的定时器:

Process.send_after 会在 xx 秒后发消息到指定的进程,通过这个功能,不断往自己发消息,从而实现定时器的功能。请看实现:

  1. defmodule Periodically do 
  2.   require Logger 
  3.   use GenServer 
  4.  
  5.   def start_link do 
  6.     GenServer.start_link(__MODULE__, %{}) 
  7.   end 
  8.  
  9.   def init(state) do 
  10.     schedule_work(:do_some_work) 
  11.     {:ok, state} 
  12.   end 
  13.  
  14.   def handle_info(:do_some_work, state) do 
  15.     doing_now() 
  16.     schedule_work(:do_some_work) 
  17.     {:noreply, state} 
  18.   end 
  19.  
  20.   defp schedule_work(update_type) do 
  21.     Process.send_after(self(), update_type, 30*1000) 
  22.   end 
  23. end 

相较于 setTimeOut 之类的,好处是什么?

Elixir 自带工具,可以查看所有进程的状态并管理,上面把 Periodically 作为一个进程启动起来了,自然可以管理他:P

let it crash 思想的提出与实现。

程序不可能处理一切错误,因此程序员只要力所能及的处理显然易见的错误就好了,而那些隐藏着的,非直觉性的错误,就让他崩掉吧 —— 本来就很有可能是极少见的错误,经常出现的?就需要程序员人工处理了,这是紧急情况,就算 try catch 所有错误也无法避免,因为系统已经陷入崩溃边缘了,苟延残喘下去只是自欺欺人。

要注意的是 let it crash ,并不是说你用 golang,C++ 等语言打包个二进制,用 supervisorctl 等工具监控,错误就让他马上崩,挂了自动重启 - = -

模式匹配与宏

这个相较于平常我们的赋值语言比较新颖,介绍的篇幅过长。

请看http://szpzs.oschina.io/2017/01/30/elixir-getting-started-pattern-matching/ 以及https://elixir-lang.org/getting-started/pattern-matching.html

通过模式匹配,我们可以避免 if else 的嵌套地狱;可以利用语言自己的匹配来做 搜索,

宏可以让你实现自定义的 DSL(当然太强大的功能自然导致滥用出 bug),可以屏蔽掉很多不优雅的细节。

以上就是 Elixir 的简单介绍,建议诸位学习一下 Elixir,洗刷一下自己的 OO 以及过程式编程思维。

本文转载自微信公众号「山尽写东西的cache」,可以通过以下二维码关注。转载本文请联系山尽写东西的cache公众号。

 

责任编辑:武晓燕 来源: 山尽写东西的cache
相关推荐

2011-08-24 09:13:40

编程

2018-11-28 11:20:53

Python函数式编程编程语言

2017-06-08 14:25:46

Kotlin函数

2022-03-26 09:06:40

ActorCSP模型

2015-11-02 17:25:23

Elixir编程语言未来

2010-08-03 08:54:07

JDK 7Lambda表达式函数式编程

2020-07-29 08:05:42

JavaScriptTypeScript工具

2022-07-07 09:03:36

Python返回函数匿名函数

2020-09-23 22:36:27

分布式架构系统

2013-09-09 09:41:34

2010-01-15 09:15:09

Scala Actor并发

2012-06-13 13:01:57

Windows Pho

2012-08-02 10:16:39

Windows Pho

2016-11-23 13:46:08

Android

2012-06-11 13:08:10

Windows Pho

2012-08-01 10:26:33

Windows Pho

2012-06-06 13:48:34

Windows Pho

2012-06-12 10:43:20

Windows Pho

2012-08-16 11:31:30

Windows Pho

2012-06-25 16:14:26

Windows Pho
点赞
收藏

51CTO技术栈公众号