iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 29
1

Elixir 繼承 Erlang 語言的特性,說平行運算是它最重要的功能也不為過。先前曾提到在 Elixir / Erlang 裡的 light-weight process,啟動一個約 1μs,運作起來像是 OO 語言裡的 instance,今天我們就來試試看。

spawn

在 Elixir 裡,啟動一個 process 非常簡單:

iex> spawn fn -> 1 + 2 end
#PID<0.92.0>

我們開了一個 process 請他計算 1 + 2。不過由於我們沒有跟它說算完之後要怎麼辦。所以它算完就把值丟掉,並且默默的離開了。

為了要有效的溝通,每個 process 都有一個名字,叫做 pid,要拿到每個 process 自己的 pid,要用 self/0。而 iex 本身,也是一個 process:

iex> self
#PID<0.88.0>  # 由於系統運作的關係,你的 pid 應該不會是同一個數值

我們想讓新的 process 將計算完的結果回傳,就是給它我們的 pid,並請它把計算後的結果傳給這個 pid

iex> pid = self
iex> spawn fn -> send(pid, 1 + 2) end
#PID<0.100.0>

雖然看起來還是什麼事都沒有發生,但其實結果已經寄到我們的信箱裡了。我們要用 receive/1 來接收這個訊息:

iex> receive do
...>   i -> IO.inspect i
...> end
3

Supervisor Tree

spawn/3send/2receive/1 是 Elixir / Erlang 裡平行運算的最基礎元素。根基於其上,Erlang 發展出一整套平行運算的理論與模式,並實作了許多可以直接拿來用的套件,這套理論與實作通稱為 OTP,是 Erlang 語言本身的一部份。

OTP 的概念是當 process 遵守一些共通的規範後,我們就可以用一致的方式啟動及關閉這些 OTP compliant process。

再進一步,我們可以做出一種專門管理其它 process 的 process,用來監視手下是否正常運作,並決定當事情不如預期時,應該如何重啟這些 process。這種 OTP process 叫做 Supervisor。

而因為 Supervisor 也是一顆 OTP process,所以 Supervisor 也可以管理 Supervisor。發展下去這些平行運算的 process 的每個環節,都有監控及從錯誤中回復的能力。而一個夠大的 Erlang / Elixir 應用程式,最後都會長成一顆 Supervision Tree。

是的。Phoenix 也是一顆 Supervision Tree。

要證明這點,我們在之前的 hello_phx 專案目錄裡,輸入這個指令:

$ iex -S mix phx.server

這個指令是先用 mix phx.server 跑起來後,再把 iex 掛載到運行中的節點上。如果是 Rails programmer 的話,你就想像是 rails srails c 的合體版。

在 iex 裡,輸入 :observer.start

iex> :observer.start

你會看到一個新的 GUI 介面,這是我們 hello_phx 系統的運作狀況:

https://ithelp.ithome.com.tw/upload/images/20180117/20103390nb7x0AJ7k0.png

切換到 [Applications],這就是這個專案裡每個 process 的從屬關係,也就是剛才所說的 Supervision Tree:

https://ithelp.ithome.com.tw/upload/images/20180117/201033906a5T2UJqqD.png

而介面裡的每個節點,都可以點進去看個別的詳細運作資訊。

更多關於 OTP

要詳述 OTP 的內容,會寫出一本非常厚的書。Elixir,特別是 Phoenix 把這件事用很優雅的方式包裝過了,因此你可以在沒有意識到這現實的情況下開發好一陣子。但想要拿到 Erlang / Elixir 系統的全部好處,或遲或早你都會接觸到這一部份。

但是別擔心。在熟悉 Elixir 一陣子後,你會發現 Erlang 及 Elixir 的哲學及處理事情的方式非常接近,很容易就能讀得懂 Erlang 的書及程式碼,屆時再來深入探索也不遲。要記得的就是 Elixir 可以輕易的使用 OTP 的全部功能。

Elixir 的包裝層

上一節說到除了 Erlang 內建的 OTP 之外,Elixir 也有一些自行包裝的函式庫。我們來看看 Task 這個用於平行計算的模組,它可以用 async/3
await/1 來進行平行運算。先新增一個檔案叫 tsk.exs,並填入以下內容

# tsk.exs
defmodule Tsk do
  def double(x) do
    :timer.sleep(1000)
    x * 2
  end
end

然後打開 iex,先編譯這個模組,接著我們用 Task.async/3 調用它:

iex> c "tsk.exs"
[Tsk]
iex> task = Task.async(Tsk, :double, [100])
%Task{
  owner: #PID<0.88.0>,
  pid: #PID<0.96.0>,
  ref: #Reference<0.3624132032.2388131842.168822>
}

接著用 Task.await/1 拿回傳的結果:

iex> Task.await(task)
200

pmap

在 Erlang 裡有個著名的函式叫 pmap/2,就是我們之前提過的 Enum.map/2,但是他會把每個元素的 map 計算,都新開一個 process 來處理。而用上 Task.async/3Task.await/1,我們也可以來做個一樣的東西。

defmodule Parallel do
  def pmap(collection, func) do
     collection
     |> Enum.map(&(Task.async(fn -> func.(&1) end)))
     |> Enum.map(&Task.await/1)
  end
end

## iex
iex> result = Parallel.pmap 1..5, &(&1 * 2))
[2, 4, 6, 8, 10]

重點回顧

  • spawn/3send/2receive/1 來開新 process
  • Elixir 可以使用 Erlang OTP 來拿到平行運算的所有好處
  • Supervision Tree
  • :observer.start可以看系統運作的狀況
  • Task 是 Elixir 包裝過的特化 OTP

Happy hacking! 明天見。


上一篇
Macro 及 web.ex
下一篇
多型: Protocol
系列文
函數式編程: 從 Elixir & Phoenix 入門。31

尚未有邦友留言

立即登入留言