11-程式

StraightDave發表於2014-09-16

11-程式

程式派生
傳送和接收
連結
狀態

Elixir中,所有程式碼都在程式中執行。程式獨立於彼此,一個接一個併發執行,彼此通過訊息傳遞來溝通。 程式不僅僅是Elixir中最基本的併發單位,它們還是Elixir建立分散式和高容錯程式的基礎。

Elixir的程式和作業系統中的程式不可混為一談。 Elixir的程式非常非常地輕量級(在使用CPU和記憶體角度上說。但是它又不同於其它語言中的執行緒)。 因此,同時執行著數萬個程式也並不是罕見的事。

本章將講解派生新程式的基本知識,以及不同程式間傳送和接受訊息。

11.1-程式派生

派生(spawning)一個新程式的方法是使用自動匯入的spawn/1函式:

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

spawn/1接收一個函式型別作為引數,在另一個程式中執行它。

注意spawn/1返回一個PID(程式標識)。在這個時候,你派生的這個程式很可能已經死了。 派生的程式執行完函式後便會結束:

iex> pid = spawn fn -> 1 + 2 end
#PID<0.44.0>
iex> Process.alive?(pid)
false

你可能會得到與例子中不一樣的PID

self/0函式獲取當前程式的PID:

iex> self()
#PID<0.41.0>
iex> Process.alive?(self())
true

可以傳送和接收訊息,程式變得越來越有趣。

11.2-傳送和接收

使用send/2函式傳送訊息,用receive/1接收訊息:

iex> send self(), {:hello, "world"}
{:hello, "world"}
iex> receive do
...>   {:hello, msg}  -> msg
...>   {:world, msg} -> "won't match"
...> end
"world"

當一條訊息被髮給某程式,該訊息被儲存在該程式的郵箱裡。receive/1語句塊 檢查當前程式的郵箱尋找匹配給定模式的訊息。receive/1函式支援很多子句,如case/2。 也可以給子句加上衛兵表示式。

如果找不到匹配的訊息,當前程式將一直等待,知道下一條資訊到達。可以設定一個超時時間:

iex> receive do
...>   {:hello, msg}  -> msg
...> after
...>   1_000 -> "nothing after 1s"
...> end
"nothing after 1s"

超時時間設為0表示你知道當前郵箱內肯定有郵件存在,很自信,設這個極短的超時時間。

把以上概念綜合起來,演示程式間傳送訊息:

iex> parent = self()
#PID<0.41.0>
iex> spawn fn -> send(parent, {:hello, self()}) end
#PID<0.48.0>
iex> receive do
...>   {:hello, pid} -> "Got hello from #{inspect pid}"
...> end
"Got hello from #PID<0.48.0>"

在shell中執行程式時,輔助函式flush/0很有用。它清空緩衝區,列印程式郵箱中的所有訊息:

iex> send self(), :hello
:hello
iex> flush()
:hello
:ok

11.3-連結

Elixir中最常用的程式派生方式是通過函式spawn_link/1。 在舉例子講解spawn_link/1之前,來看看如果一個程式失敗了會發生什麼:

iex> spawn fn -> raise "oops" end
#PID<0.58.0>

。。。啥也沒發生。這時因為程式都是互不干擾的。如果我們希望一個程式中發生失敗可以被另一個程式知道,我們需要連結它們。 使用spawn_link/1函式,例子:

iex> spawn_link fn -> raise "oops" end
#PID<0.60.0>
** (EXIT from #PID<0.41.0>) an exception was raised:
    ** (RuntimeError) oops
        :erlang.apply/2

當失敗發生在shell中,shell會自動終止執行,並顯示失敗資訊。這導致我們沒法看清背後過程。 要弄明白連結的程式在失敗時發生了什麼,我們在一個指令碼檔案使用spawn_link/1並且執行和觀察它:

# spawn.exs
spawn_link fn -> raise "oops" end

receive do
  :hello -> "let's wait until the process fails"
end

這次,失敗的程式在失敗時把它的父程式也弄停止了,因為它們是連結的。 手動連結程式:Process.link/1。 建議可以多看看Process模組,裡面包含很多常用的程式操作函式。

程式和連結在建立能容忍失敗的系統時扮演重要角色。在Elixir程式中,我們經常把程式連結到某“管理者”上。 由這個角色負責檢測失敗程式,並且建立新程式取代之。因為程式間獨立,預設情況下不共享任何東西。 而且當一個程式失敗了,也不會影響其它程式。 因此這種形式(程式連結到“管理者”角色)是唯一的實現方法。

其它語言通常需要我們來try-catch異常,而在Elixir中我們對此無所謂,放手任程式掛掉。 因為我們希望“管理者”會以更合適的方式重啟系統。 “要死你就快一點”是Elixir軟體開發的通用哲學。

在講下一章之前,讓我們來看一個Elixir中常見的建立程式的情形。

11.4-狀態

目前為止我們還沒有怎麼談到狀態。但是,只要你建立程式,就需要狀態。 例如,儲存程式的配置資訊,或者分析一個檔案先把它儲存在記憶體裡。 你怎麼儲存狀態?

程式就是(最常見的)答案。我們可以寫無限迴圈的程式,儲存一個狀態,然後通過收發資訊來告知或改變該狀態。 例如,寫一個模組檔案,用來建立一個提供k-v倉儲服務的程式:

defmodule KV do
  def start do
    {:ok, spawn_link(fn -> loop(%{}) end)}
  end

  defp loop(map) do
    receive do
      {:get, key, caller} ->
        send caller, Map.get(map, key)
        loop(map)
      {:put, key, value} ->
        loop(Map.put(map, key, value))
    end
  end
end

注意start函式簡單地派生一個新程式,這個程式以一個空的圖為引數,執行loop/1函式。 這個loop/1函式等待訊息,並且針對每個訊息執行合適的操作。 加入受到一個:get訊息,它把訊息發回給呼叫者,然後再次呼叫自身loop/1,等待新訊息。 當受到:put訊息,它便用一個新版本的圖變數(裡面的k-v更新了)再次呼叫自身。

執行一下試試:

iex> {:ok, pid} = KV.start
#PID<0.62.0>
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.41.0>}
iex> flush
nil

一開始程式內的圖變數是沒有鍵值的,所以傳送一個:get訊息並且重新整理當前程式的收件箱,返回nil。 下面再試試傳送一個:put訊息:

iex> send pid, {:put, :hello, :world}
#PID<0.62.0>
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.41.0>}
iex> flush
:world

注意程式是怎麼保持一個狀態的:我們通過同該程式收發訊息來獲取和更新這個狀態。 事實上,任何程式只要知道該程式的PID,都能讀取和修改狀態。

還可以註冊這個PID,給它一個名稱。這使得人人都知道它的名字,並通過名字來向它傳送訊息:

iex> Process.register(pid, :kv)
true
iex> send :kv, {:get, :hello, self()}
 {:get, :hello, #PID<0.41.0>}
iex> flush
:world

使用程式維護狀態,以及註冊程式都是Elixir程式非常常用的方式。 但是大多數時間我們不會自己實現,而是使用Elixir提供的抽象實現。 例如,Elixir提供的agent就是一種維護狀態的簡單的抽象實現:

iex> {:ok, pid} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.72.0>}
iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
:ok
iex> Agent.get(pid, fn map -> Map.get(map, :hello) end)
:world

Agent.start/2方法加一個一個:name選項可以自動為其註冊一個名字。 除了agents,Elixir還提供了建立通用伺服器(generic servers,稱作GenServer)、 通用時間管理器以及事件處理器(又稱GenEvent)的API。 這些,連同“管理者”樹,都可以在Mix和OTP手冊裡找到詳細說明。

相關文章