Erlang/Elixir 中的 OTP 編程介紹

前言

接觸 Elixir 也有必定的時間了(接近一個月了),這是一門我很是看好的語言,它有使人舒服的語法和友好的編程方式以及強大優雅的 Erlang 併發設計。編程

其實在最初我沒想過要接觸 Erlang,我之因此選擇 Elixir 而不是 Erlang 也是由於「道聽途書」本身給 Erlang 扣上了莫虛烏有的「語法怪異」的帽子。語法怪異就會產生更多的擔憂:是否是寫起代碼來很囉嗦、很別捏?
而且在我大概用兩天時間學完 Elixir 的基礎內容,比較充分的體會到 Elixir 的優雅以後就便更加擔憂 Erlang 是否是設計落後因此纔有了 Elixir?設計模式

直到終於由於我沒法理解 Elixir 官方指南中的 OTP 編程,我才明白不學 Erlang 就企圖完全搞懂 OTP 設計是一種「妄想」。基於這個緣由才致使我正式接觸了 Erlang 以及原生的 TOP,也正是這個決定讓我理解了什麼是 OTP 編程的同時還避免由於「誤解」而錯過 Erlang 這麼優秀的語言。bash

因此我最初決定接觸 Erlang 的理由,即是這篇文章的主題:Elixir 中的 OTP 編程是什麼?我會盡量的以 Elixir 角度來剖析,並帶入 Erlang 中的設計原則。畢竟不是每個 Elixir 開發者都必須是 Erlang 的用戶,這是加分項但不是必選項。服務器

OTP 概念

OTP 是 Open Telecom Platform(開放電信平臺)的縮寫。這個命名的由來可能跟 Erlang 最初服務的業務相關,畢竟 Erlang 曾經是通訊行業巨頭愛立信全部的私有軟件。實際上後來 OTP 的設計和功能已經脫離了它名稱的本意,因此 OTP 應該被看做一種名意無關的概念。併發

在 Erlang/Elixir 中也許你已經能夠利用語言內置的功能來實現一些常見的併發場景,可是假設每一個人每一個項目都要這麼作一遍或者多遍那就顯得太多餘了。做爲一個有足夠編碼經驗的你必定能想到能夠將它們組合起來抽象成爲適用必定場景或儘量通用的「框架」,這即是 OTP。使用 OTP 只須要實現 OTP 的行爲模式並基於行爲模式的 API 設計做爲通訊細節,即可以涵蓋到各類場景下,讓開發者更專一業務的實現,沒必要爲併發和容錯而擔心。app

OTP 應用

與大多數程序以及編程語言相反,OTP 應用自己不具有一個阻塞程序執行的主執行流(線程/進程之類的並行單元)。準確的說是 OTP 應用自身的進程並不阻塞應用,Erlang 的面向進程編程即是這個的前提。框架

對於 OTP 應用而言,應用自己是由多個進程組成的,通常來說是一種監督樹結構,這些進程會出現不一樣的分工但不會具有任何特權。與之相對的,例如常規程序是由一個啓動應用的線程阻塞來維持運行的,若是這個線程結束了那麼程序就結束了(一般全部的後臺線程會被釋放)。可是 OTP 應用是由 ERTS(Erlang 運行時系統) 來加載啓動的,每個進程都是平等的,你會發現其實每個 OTP 應用都相似於由多個微服務(進程)組成的系統,面向進程編程就是在這個系統上開發出一個個的「微服務」,具有這個原則設計的程序即是 OTP 應用。async

咱們用實際代碼來舉例,首先咱們建立一個 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 中執行的進程阻塞了,且該進程徹底不受任何管理。

(注意,你徹底也能夠經過 escript 來模擬這個程序,會更加直觀。只不過 Elixir 的 escrit 須要 mix 支持,反而不直觀)

接着咱們再實現一個相同功能的程序,可是以 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 函數在啓動監督進程之後就當即結束了,返回了相應的結果。因此此時監督進程和被監督的進程都是後臺運行狀態,而且進程之間被正確的組織起來管理。

PS:實際上 hello_otp 的監督進程和 iex 終端進程是平級的關係。

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 實踐講解會新開一篇文章。

1、監督樹

監督樹對於 OTP 而言是很是重要的一個概念,也是 OTP 實現「高容錯」保證的基石。簡單來說,監督樹是一種組織進程的方式,由於進程總體是一個樹結構,而根是又一個最頂級的監督進程,因此稱做「監督樹」。

PS:構建監督樹須要 Supervisor 模塊

借用官網的一張圖:

其中方框表示監督進程,圓圈表示工做進程。監督進程又能夠監督下一級的監督進程,每個工做進程又被本身的監督進程監督,像極了企業中老闆、管理層和普通員工的關係。每個監督者均可以定義被監督進程的重啓策略,每個工人又能夠定義本身的重啓時機。複雜能夠配置一套高度定製化的容錯機制,簡單能夠進行「永久運行」保證。

對了,上面實現的 hello_otp 應用是最簡單的根監督進程 + 一個工做進程的結構,可是若是咱們將工做進程殺死,Hello! 不會再輸出了。嗯…… 好像哪裏不對的樣子 (⊙?⊙) 按理說不該該會當即重啓而後繼續輸出嗎?監督進程不就是幹這種事的麼?有關爲何 hello_otp 應用的工做進程被殺死卻不重啓的緣由這裏暫且不提,看了下一篇就會明白了:)

2、通用服務器

在平常開發中,若是要實現一個基礎服務器,須要涉及到狀態維護、進程建立、持續接收和響應消息以及進程退出等功能。而 OTP 的通用服務器(GenServer 模塊)就是對 客戶端 - 服務端 模型的封裝,通用服務器不只能夠簡單可靠的做爲 C/S 模型依賴,其自己也是實現其它部分 OTP 行爲模式的基礎。

最簡單的 C/S 模型示意圖(摘自官網文檔):

通用服務器也是體現 OTP 核心目的的最典型例子,即:提取通用的代碼/組件進行抽象,並儘量的重用它們。

3、狀態機

狀態機(gen_statem)跟通用服務器(gen_server) 同樣是 OTP 標準行爲模式之一,對狀態機業務流程模擬的典型例子就是「開門/關門」:

上圖摘自一篇介紹 Drupal 工做流的文章(不是我懶得畫圖,而是有關狀態機的概念描述已經夠多了,我沒必要屢次一舉)。在這個例子中,門會根據輸入轉換爲三種狀態(開啓、關閉和鎖定),不過門須要從鎖定狀態(Locked)轉換爲關閉(Closed)狀態之後才能打開(Opened),不能將一個上鎖的門在不通過解鎖的狀況下直接打開,即從 Locked 直接轉換爲 Opened。

注意:Elixir 並無對 Erlang 的 gen_statem 模塊進行包裝,另外 Erlang 19.0 以前提供的相關行模式爲 gen_fsm。

gen_statem 模塊跟 GenServer 模塊的設計很類似,而且在必定程度上 GenServer 也能解決相似業務。在官方的建議中,若是業務流程足夠簡單,而且將來也不會遇到須要實現 gen_statem 行爲模式才能徹底適應你的問題的情形,那麼僅使用 GenServer 便可。

4、事件管理器

事件管理器(gen_event)也是 OTP 標準行爲模式,其 API 設計跟通用服務器(gen_server)類似,可是運做方式卻不一樣。

事件管理器之因此叫「管理器」是由於它並不直接處理事件,而是管理事件「處理器」,而事件處理器纔是實現 gen_event 行爲模式的具體模塊。事件管理器本質上是一個維護 {Module,State} 對的列表,Module 即事件處理器,State 是處理器的內部狀態。
在監督樹中,每每只用啓動一個 gen_event 事件管理器,而後「熱插拔」多個事件處理器。在須要的時候添加,不須要的時候刪除。正是這種一對多的關係決定了它與通用服務器的運做方式的不一樣。

最後

Erlang 和 Elixir 都是很是不錯的語言,OTP 這一套更是 Erlang/Elixir 堅強的後盾。使用 OTP 原則設計應用程序,能足夠保證程序的健壯性,由於 OTP 通過嚴格而充分的測試。而且應用 OTP 的行爲模式,能大大提升程序的可讀性,畢竟它們是人盡皆知(步入 Erlang 的必經之路)的設計模式。有關 OTP 行爲模式具體案例的講解,我會再下一篇發出來,而本文也到此未知:)

最後歡迎小夥伴加 Telegram 羣學習和交流(Erlang/Elixir):https://t.me/elixir_cn

相關文章
相關標籤/搜索