内容简介:接触 Elixir 也有一定的时间了(接近一个月了?),这是一门我非常喜欢的语言,它有令人舒服的语法和编程方式以及强大优雅的 Erlang 并发设计。其实在最初我没想过要接触 Erlang,我之所以选择 Elixir 而不是 Erlang 也是因为“道听途书”自己给 Erlang 扣上了莫虚乌有的“语法怪异”的帽子。语法怪异就会产生更多的担心:是不是写起代码来很啰嗦、很别捏?并且在我大概用两天时间学完 Elixir 的基础内容,比较充分的体会到 Elixir 的优雅之后就便更加担心 Erlang 是不是设
接触 Elixir 也有一定的时间了(接近一个月了?),这是一门我非常喜欢的语言,它有令人舒服的语法和编程方式以及强大优雅的 Erlang 并发设计。
其实在最初我没想过要接触 Erlang,我之所以选择 Elixir 而不是 Erlang 也是因为“道听途书”自己给 Erlang 扣上了莫虚乌有的“语法怪异”的帽子。语法怪异就会产生更多的担心:是不是写起代码来很啰嗦、很别捏?
并且在我大概用两天时间学完 Elixir 的基础内容,比较充分的体会到 Elixir 的优雅之后就便更加担心 Erlang 是不是设计落后所以才有了 Elixir?
直到终于因为我无法理解 Elixir 官方指南中的 OTP 编程,我才明白不学 Erlang 就企图彻底搞懂 OTP 设计是一种“妄想”。基于这个原因才导致我正式接触了 Erlang 以及原生的 TOP,也正是这个决定让我理解了什么是 OTP 编程的同时还避免因为“误解”而错过 Erlang 这么优秀的语言。
所以我最初决定接触 Erlang 的理由,便是这篇文章的主题:Elixir 中的 OTP 编程是什么?我会尽可能的以 Elixir 角度来剖析,并带入 Erlang 中的设计原则。毕竟不是每一个 Elixir 开发者都必须是 Erlang 的用户,这是加分项但不是必选项。
OTP 概念
OTP 是 Open Telecom Platform(开放电信平台)的缩写。这个命名的由来可能跟 Erlang 最初服务的业务相关,毕竟 Erlang 曾经是通信行业巨头爱立信所有的私有软件。实际上后来 OTP 的设计和功能已经脱离了它名称的本意,所以 OTP 应该被看作一种名意无关的概念。
在 Erlang/Elixir 中也许你已经可以根据语言内置的功能实现一些常见的并发场景,但是假设你每次都需要手动实现一遍或者多遍那就显得太多余了,没错你一定想到了可以将它们组合起来抽象成为适用一定场景或尽可能通用的“框架”,这便是 OTP。使用 OTP 只需要实现 OTP 的行为模式并基于行为模式的 API 设计作为通信细节,便可以涵盖到各种场景下,让开发者更专注业务的实现,不必为并发和容错而担忧。
OTP 应用
与大多数程序以及编程语言相反,OTP 应用本身不具备一个阻塞程序执行的主执行流(线程/进程之类的并行单元)。准确的说是 OTP 应用自身的进程并不阻塞应用,Erlang 的面向进程编程便是这个的前提。
对于 OTP 应用而言,应用本身是由多个进程组成的,通常来讲是一种监督树结构,而这些进程除了分工不同之外并不具备任何特权。与之相对的,例如常规程序是由一个启动应用的线程阻塞来维持运行的,如果这个线程结束了那么程序就结束了(通常所有的后台线程会被释放)。但是 OTP 应用是由 ERTS(Erlang 运行时系统) 来加载启动的,每一个进程都是平等的,你会发现其实每一个 OTP 应用都类似于由多个微服务(进程)组成的系统,面向进程编程就是在这个系统上开发出一个个的“微服务”,具备这个原则设计的程序便是 OTP 应用。
我们用实际代码来举例,首先我们创建一个 hello_main 项目:
mix new hello_main
修改 lib/hello_main.ex 文件,添加一个用作启动的入口函数(main/0),逻辑为调用一个无限递归的输出 Hello! 字符串的函数(loop_echo/1):
defmodule HelloMain do def main do loop_echo("Hello!") end def loop_echo(text) do IO.puts(text) :timer.sleep(1000) loop_echo(text) end end
执行(启动)这个程序:
iex -S mix run -e HelloMain.main
我们会看到如下输出:
Erlang/OTP 21 [erts-10.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] Compiling 1 file (.ex) Generated hello_main app Hello! Hello! Hello! # ……
注意了,这时候我们的 iex 被 main 中执行的进程阻塞了,且完全不受管理。
接着我们再实现一个相同功能的程序,但是以 OTP 的原则来进行组织。创建 hello_otp 项目:
mix new hello_otp
修改 mix.exs 添加回调模块:
def application do [ mod: {HelloOtp, []}, # …… ] end
给 lib/hello_otp.ex 添加相同的 loop_echo 函数,并实现 Application 行为模式:
defmodule HelloOtp do use Application def start(_type, _args) do children = [{Task, fn -> loop_echo("Hello!") end}] Supervisor.start_link(children, strategy: :one_for_one) end # loop_echo/1 defined here …… end
启动应用(注意因为我们实现了 Application 并定义了回调模块,不需要手动指定启动函数):
iex -S mix
在这里,我们使用了监督进程来启动和管理调用 loop_echo 函数的进程。并且由于监督进程并不会阻塞 iex 前台,所以输出 Hello! 的同时还能正常使用 iex 的功能。
两个程序的不同之处在于,hello_main 的整个执行周期都不会将入口函数 main 执行完毕,因为这是一个不可能返回的函数逻辑,哪怕强行终止程序。而 hello_opt 的 start/2 函数在启动监督进程以后就立即结束了,返回了相应的结果。所以此时监督进程和被监督的进程都是后台运行状态,并且进程之间被正确的组织起来管理。
hello_otp 的特点是正确的实现了 Application 行为模式(返回了结果),整个应用是由一个或多个进程组成,每个进程都在后台运行,能便捷的被 ERTS 管理。
而 hello_main 更接近于我们所见到的常规程序,第一个启动的进程阻塞执行,它结束应用便结束。相信看到这里,你应该大概能明白 OTP 应用的定义了。
OTP 应用本质
如果你接触过 Erlang 并组织过 OTP 应用,那你应该知道每一个应用都存在一份“规范”,这份规范会被 Application 模块载入(对于上述的 hello_otp 应用而言在载入之后还会被回调指定函数)。
我们脱离 mix 手动调用模块来重现这一点,不过前提是确保你的程序已经经过编译:
mix compile
直接运行 iex(或者 erl):
iex -pa _build/dev/lib/hello_otp/ebin/
(-pa 参数是将手动指定的路径添加到模块搜索路径的列表中,这样子才可以在载入时找到我们自己的模块)
跟之前不同的是,这个时候 iex 的控制台并没有输出 Hello!,因为应用没有被载入更不会被启动,我们要手动做这一步:
Application.start :hello_otp # 如果是 erl 则使用 application:start(hello_otp)
控制台会打印一个 :ok(Application.start/1 函数返回值),然后不断的输出 Hello!,跟使用 mix 启动的效果是一样的,只是这些步骤被 mix run 做了而已。
别忘了上面提过,每一个 OTP 应用都有一份“规范”文件,Application.start/1 函数首先做的就是寻找这份规范文件,然后根据解析结果载入模块。我们可以从 _build/dev/lib/hello_otp/ebin 目录中看到一个名为 hello_otp.app 的文件,这便是所谓的“规范”文件。它的格式是一个 Erlang 元组,其中 mod 定义了入口模块(也就是之前 mix.exs 中添加过的),之所以执行 Application.start(:hello_otp) 会回调 HelloOtp.start/2 函数也是这个原因。
这让我们明白了,OTP 应用其实就是被 ERTS 载入的一系列模块,应用启动的进程由实现 Application 行为模式的入口模块在回调函数的执行过程中产生。
那么,不产生进程的但符合 OTP 应用结构的模块被载入以后,它算不算 OTP 应用呢?答案是:算。不产生进程的 OTP 应用很常见,那就是“库”应用。实际上我们也能将 hello_otp 作为库应用载入(重新进入 iex):
Application.load :hello_otp
调用 Application.load/1 函数发现同样返回了 :ok,不过没有任何 Hello! 产生,因为并没有回调 HelloOtp.start/2 函数。此时你可以手动调用 HelloOtp.start/2 或者 HelloOtp.loop_echo/1 函数,聪明的你一定意识到了,这时候的 hello_otp 便成为了一个“库应用”。如果你想让这个库产生进程,即启动 hello_otp 程序,只需要:
HelloOtp.start(:normal, [])
手动调用 HelloOtp.start/2 函数即可。也就是说对于 Erlang/Elixir 而言,更加是对于 OTP 原则组织的模块而言,库和具有入口的程序区别不大,它们都被称之为“应用”。
所以写到这里有必要推翻上面说过的 OTP 应用启动会产生一个或多个后台进程,这并不是必须的。如果要明确的定义某个程序是否属于 OTP 应用,只需要从它的模块组织上来看就行了,其运行过程并不重要。但是即便模块组织上符合规范,仍然可能存在有问题的 OTP 应用:例如不正确的实现行为模式。如果我在 start/2 函数中不启动监督进程,而是直接调用 loop_echo/1,这样的做法会导致前台进程阻塞,start/2 回调函数永远无法返回,和 hello_main 也没多少区别了。
OTP 设计原则
终于讲到这里了,OTP 究竟是怎样设计的?它的设计分别落实到那些实体概念?要深入讲解 OTP 其实有很多细节需要描述,而这一节只是对 OTP 的设计做一个大体概括。具体的 OTP 讲解会新开一篇文章。
一、监督树
监督树是 OTP 中非常重要的一个概念,也是 OTP 实现“高容错”保证的基石。简单来讲,监督树是一种组织进程的方式,因为进程整体是一个树结构,而根是又一个最顶级的监督进程,所以称作“监督树”。
借用官网的一张图:
其中方框表示监督进程,圆圈表示工作进程。监督进程又可以监督下一级的监督进程,每一个工作进程又被自己的监督进程监督,像极了企业中老板、管理层和普通员工的关系。每一个监督者都可以配置自己的重启策略,每一个工人又可以配置自己的重启时机,总体来说即可以配置一套高度定制化的容错机制,也可以简单的进行“永久运行”保证。
对了,上面实现的 hello_otp 应用是最简单的一个根监督进程 + 一个工作进程的结构,但是如果我们将工作进程杀死,Hello! 不会再输出了。嗯…… 好像哪里不对的样子 (⊙?⊙) 按理说不应该会立即重启然后继续输出吗?监督进程不就是干这种事的么?有关为什么 hello_otp 应用的工作进程被杀死却不重启的原因这里暂且不提,看了下一篇就会明白了:)
努力更新中……
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。