本文讲了最近很火的 Elixir,也讲了我对编程语言选择的看法。同时,作为一个 Erlang 爱好者,Elixir 也是不能忽视的。即便是这么多编程语言,Elixir 还是值得入手的。
Elixir 并不是一门新兴的语言,不过近来 Elixir 的生态逐渐完善,越来越多的高手开始关注这门语言,并给予 Elixir 良好的评价。
立即开始使用 Elixir
一个小的 Elixir 示例
并行处理JSON字符串输入,解析为可用变量Elixir Elixir: 编程语言的未来,计算每秒的处理速度并输出。
使用 entop 监控 Elixir 应用程序状态
关于编程语言选择的一些思考
作为一名语言爱好者,我接触过不少各种风格的编程语言,比如Java、Erlang、Scala、PHP、C#、C、Python、Ruby等等。有人说学习这么多编程语言就是为了当“翻译官”?其实没那么简单。
不同语言背后是完全不同的库、技术栈、生态、工具链。不同语言针对不同类型的问题,用某些语言解决某些问题的成本比其他语言低很多。本质上,学习编程语言就是为了低成本、高效率地解决实际业务问题。
我最喜欢的编程语言风格
更改可以近乎实时地更新
如果应用程序不需要很长的编译时间来执行并且可以快速启动,那就最好了。
Java 和 C 的编译速度很慢,不适合经常修改的项目。但是 PHP 和 Node.js 的改动是即时可见的,可以大大提高开发效率。最好它们也能像很多前端工具一样支持热加载,这样只要源代码有一点改动,就会自动反映在浏览器中,而不需要刷新页面。Play 框架也有类似的自动加载功能。
此外,如果能在生产环境实现热重载就更好了,这样更新的代码就不会影响用户了。很多人对此感到高兴,因为 PHP 默认就是这样的,部署后通过刷新 APC 缓存就可以实现。
这正是无状态、短链接 HTTP 应用的优势,虽然由于更多的 TCP 开销,性能相对有所降低,但却让问题变得简单很多。然而,很多其他语言很难做到这一点,比如大多数 Java 应用。
Elixir 和 Erlang 确实可以使在任何情况下更换跑车的车轮成为可能。
关于热加载,可以参看另一篇文章:编程开发中常用的热加载工具。
支持并发执行
人们比较习惯顺序执行的思想,大部分业务逻辑都是顺序执行的。但是为了减少延迟,提高性能,最好在语言层面支持并发执行。比如,在一个操作开始并返回结果之前,可以启动另一个操作。
这样,调用远程 API 或远程 RPC 所花费的时间就是最慢操作所花费的时间。从这个角度来看,大多数流行语言都可以进行并发调用,但 PHP 很难做到这一点。
轻量级执行进程或线程
有些业务逻辑由于一定的限制,由于计算量大、网盘IO等原因,必然会占用一个执行进程或者线程,所以希望这个执行体能够尽量轻量,占用内存少,启动时间快,切换消耗小,最好是执行IO的时候自动让出计算资源。
并发和并行
我们更加注重并发,而较少关注并行,因为通过增加机器来应对大量的用户请求比节省机器更加容易,也更加紧迫。
这也是目前很多互联网公司有几百台、几千台服务器的现状,由于业务逻辑不同,很难比较用户数、请求数,所以只能比较机器数。
并发进程模型
PHP 就是这种模式的典型,我见过某个异步的 PHP 框架,它的 CPU 占用非常高,甚至高于业务逻辑的 CPU 占用。
并发线程模型
该模型比进程模型好得多,因为线程比进程轻得多,并且可以更快地创建和切换。
问题:线程与内核线程是多对多的关系,内核线程有限,可调度的用户线程数量有限,无法充分发挥多核性能,创建新线程消耗大量资源,IO阻塞无法释放计算资源。
每个CPU核心同一时间只能运行一个线程,多个线程需要切换调度(CS)。如果计算是CPU密集型的,且没有或很少IO操作,最好启动相同CPU核心数的线程。
但是如果有IO操作,比如磁盘或者网络,那么比CPU核心数更多的线程是有效的,因为当进行IO操作时,有可能切换到其他线程来执行CPU操作。
并发Fork-join轻量级进程模型:
Fork-join 创建自己的进程池来执行小粒度的任务。
相比于Erlang的VM实现的真正的抢占式调度,或者操作系统的抢占式调度,Fork-join模型非常简单Elixir,这也意味着它相比之下效率相对较低。
Fork-join是针对计算密集型操作而设计的,也就是说你无法因为IO等待而告诉F/J框架暂时释放计算资源,所以一般需要把异步IO操作放到另外一个线程池中,FJ只处理纯计算。
基于Scala的Akka就是这样的模型。因此,如果处理不好,Akka的Actor很容易阻塞执行线程。如果执行线程池中的线程耗尽Elixir,整个应用程序就会卡在那里。而Erlang则不存在这个问题。
Erlang轻量级并发进程模型:
VM 调度线程,将计算划分为非常小的执行单元,可以支持大量进程,IO 阻塞可以自动释放资源,是真正的抢占式调度。
类型系统
静态类型可以避免许多错误。动态类型经常会导致意想不到的结果,这违背了 UNIX 风格的最小意外原则。
动态类型可以加快开发速度。强静态类型系统执行速度非常快,例如 JavaElixir Elixir: 编程语言的未来,但在必要时也可以使用反射,例如在很多 RPC 框架的实现中(当然还有进一步的字节码修改技术)。
每种语言的类型系统都有其自身的特性。
丰富的内置结构或容器类
最好能够区分Interface、Struct和Implementation,这样就可以很方便的以相对统一的模式定义你需要的结构。
气相色谱系统
除非 Erlang 拥有无与伦比的轻量级线程级 GC,否则您要么需要记住和理解复杂的 GC 调优参数,要么像 PHP 一样终止进程并在一段时间后重新启动它。
元编程和 DSL 可扩展性
语法层面的抽象和封装,可以提高开发效率。如何在Elixr中实现DSL。
执行速度和性能
这与并发并行模式和多核利用率密切相关。
UNIX 风格
简单来说就是模块化,每个模块完成相对单一的功能,通过多个模块组合完成复制任务,项目设计就像搭积木,不同模块的输入输出是可以拼接的。
另外一种就是简约风格。
依赖和库管理系统
Node.js npm 是最好的依赖管理系统,这导致 Node.js 社区库的数量激增。由于创建和发布库非常容易,因此查找所需的库也非常简单。
大大提高了开发效率。
包装及配送系统
最好打包成单个文件,方便分发部署,比如Java应用打包成Fat Jar包到处执行,或者像Golang那样编译成单个文件。
日志系统
真实的项目,日志非常重要,前面的文章已经提到了日志的重要性,因此一个好的内置日志系统或者说一个相对统一高效的日志模式非常重要。
最好支持屏幕打印,写文件等,这个可能不算是编程语言的特性,要看语言有没有好的日志库。
Java的SLF是一个比较优秀的日志系统库。
工具链
项目构建、编译和测试工具比较完善。
比如Java、Scala项目可以使用maven、sbt,Erlang项目可以使用rebar,但是Elixir的mix要友好很多倍。
另外,一个好的 REPL 命令行工具非常重要,因为它可以轻松地分解到应用程序中进行调试,或者测试代码片段。
例如PHP的php -a、sbt,Clojure的lein,Erlang的erl,Elixir的iex等等。
脚本执行
这是脚本语言的一大优势,小任务可以创建脚本立即执行,无需修改、编译或部署现有的正在运行的应用程序。
这对于小任务来说非常重要。Erlang 和 Elixir 都支持像这样运行 escript 或 Elixir 脚本。例如,连接到集群,读取状态或执行一次性数据操作,然后断开连接。
测试系统
最好有一个相对标准的单元测试模型,例如Java,Node.js,Scala,Elixir等。
说了这么多,我们回到Elixir,首先Elixir的执行和Erlang没什么区别,Elixir具备Erlang所有的优点,比如真正的抢占式调度;充分利用多核并行执行;Actor模型;监控树;透明分布;
极高的稳定性;代码热更新部署;函数式编程;模式匹配;等等。而且很多Erlang工具也可以直接使用,比如entop。
Elixir 在语法、工具链和社区方面比 Erlang 更具优势。许多以前写 Ruby 的人开始写 Elixir,因为它们的语法最接近。
Elixir 中的元编程和 DSL
1. quote 将代码转换为 AST,非常类似于 LISP 语法。
引用 do: 1 + 2
2. 执行引用表达式
Code.eval_quoted(引用执行:1 + 2)
3. unquote 用于引用引用范围之外的变量
数量 = 13
Macro.to_string(引用执行:11 + unquote(number))
Elixir 成熟工具链
mix:项目创建和构建工具
hex:与 npm 类似的依赖和库管理系统
iex:与Erlang的erl类似,既是EPRL,又是应用程序启动命令
exunit:单元测试工具
提示:(Erlang\Elixir\Akka都需要注意不要让某个Actor的Queue堆积过多的消息而成为系统瓶颈,监控Queue长度是非常必要的。)
Erlang 和 Elixir 的一些有用工具和库
恩托普
进程
:观察者.启动()
钢筋