檢視原始碼 程序
在 Elixir 中,所有程式碼都在程序中執行。程序彼此隔離,並行執行,並透過訊息傳遞進行通訊。程序不只是 Elixir 中並行的基礎,它們也提供建置分散式和容錯程式的管道。
Elixir 的程序不應與作業系統程序混淆。Elixir 中的程序在記憶體和 CPU 方面極為輕量級(即使與許多其他程式語言中使用的執行緒相比也是如此)。因此,同時執行數十萬甚至數百萬個程序並不少見。
在本章中,我們將瞭解產生新程序的基本結構,以及在程序之間傳送和接收訊息的方法。
產生程序
產生新程序的基本機制是自動匯入的 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
注意:您可能會取得與我們在本指南中取得的不同的程序識別碼。
我們可以透過呼叫 self/0
來擷取目前程序的 PID
iex> self()
#PID<0.41.0>
iex> Process.alive?(self())
true
當我們能夠傳送和接收訊息時,程序會變得更有趣。
傳送和接收訊息
我們可以使用 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
。
傳送訊息的程序不會在 send/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>"
函數 inspect/1
用於將資料結構的內部表示轉換為字串,通常用於列印。請注意,當 receive
區塊執行時,我們已產生的傳送程序可能已經死亡,因為其唯一的指令是傳送訊息。
在 shell 中,您可能會發現輔助函數 flush/0
非常有用。它會沖刷並列印信箱中的所有訊息。
iex> send(self(), :hello)
:hello
iex> flush()
:hello
:ok
連結
在 Elixir 中,我們大多數時候會以連結程序的方式產生程序。在我們展示 spawn_link/1
的範例之前,讓我們看看使用 spawn/1
啟動的程序發生故障時會發生什麼情況
iex> spawn(fn -> raise "oops" end)
#PID<0.58.0>
[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
它只記錄了一個錯誤,但父程序仍在執行。這是因為程序是孤立的。如果我們希望一個程序中的故障傳播到另一個程序,我們應該將它們連結起來。這可以使用 spawn_link/1
來完成
iex> self()
#PID<0.41.0>
iex> spawn_link(fn -> raise "oops" end)
** (EXIT from #PID<0.41.0>) evaluator process exited with reason: an exception was raised:
** (RuntimeError) oops
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
[error] Process #PID<0.289.0> raised an exception
** (RuntimeError) oops
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
由於程序是連結的,我們現在看到一個訊息,指出父程序(即 shell 程序)已從另一個程序收到 EXIT 訊號,導致 shell 終止。IEx 會偵測此情況並啟動新的 shell 會話。
也可以透過呼叫 Process.link/1
手動進行連結。我們建議您查看 Process
模組,以了解程序提供的其他功能。
在建立容錯系統時,程序和連結扮演著重要的角色。Elixir 程序是孤立的,並且預設不共用任何內容。因此,一個程序中的故障絕不會使另一個程序崩潰或損壞其狀態。然而,連結允許程序在發生故障時建立關係。我們通常將程序連結到監督程序,監督程序會偵測程序何時死亡並在原處啟動新的程序。
雖然其他語言要求我們捕捉/處理例外,但在 Elixir 中,我們實際上很樂意讓程序失敗,因為我們希望監督程序適當地重新啟動我們的系統。「快速失敗」(有時稱為「讓它崩潰」)是撰寫 Elixir 軟體時常見的哲學!
spawn/1
和 spawn_link/1
是在 Elixir 中建立程序的基本原語。雖然到目前為止我們一直專門使用它們,但大多數時候我們會使用建立在其上的抽象化。讓我們看看最常見的一個,稱為任務。
任務
任務建立在產生函數之上,以提供更好的錯誤報告和內省
iex> Task.start(fn -> raise "oops" end)
{:ok, #PID<0.55.0>}
15:22:33.046 [error] Task #PID<0.55.0> started from #PID<0.53.0> terminating
** (RuntimeError) oops
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
(elixir) lib/task/supervised.ex:85: Task.Supervised.do_apply/2
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Function: #Function<20.99386804/0 in :erl_eval.expr/5>
Args: []
取代 spawn/1
和 spawn_link/1
,我們使用 Task.start/1
和 Task.start_link/1
,它們會傳回 {:ok, pid}
而非僅傳回 PID。這使得任務得以用於監督樹狀結構。此外,Task
提供便利函式,例如 Task.async/1
和 Task.await/1
,以及簡化分派的實用功能。
我們將在 "Mix 和 OTP 指南" 中探討任務以及圍繞程序的其他抽象概念。
狀態
到目前為止,我們尚未在本指南中討論狀態。如果你正在建置需要狀態的應用程式,例如,為了保留應用程式組態,或者你需要剖析檔案並將其保留在記憶體中,你會將其儲存在哪裡?
程序是這個問題最常見的答案。我們可以撰寫無限迴圈的程序,維護狀態,並傳送和接收訊息。舉例來說,我們撰寫一個模組,啟動新的程序,這些程序會在名為 kv.exs
的檔案中擔任鍵值儲存。
defmodule KV do
def start_link do
Task.start_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_link
函式會啟動一個新的程序,執行 loop/1
函式,從一個空的映射開始。loop/1
(私人)函式接著會等待訊息,並針對每則訊息執行適當的動作。我們使用 defp
取代 def
,讓 loop/1
成為私人函式。在 :get
訊息的情況下,它會傳送一則訊息回呼叫方,並再次呼叫 loop/1
,以等待新的訊息。而 :put
訊息實際上會呼叫 loop/1
,並提供映射的新版本,其中儲存了指定的 key
和 value
。
讓我們執行 iex kv.exs
來試試看
iex> {:ok, pid} = KV.start_link()
{:ok, #PID<0.62.0>}
iex> send(pid, {:get, :hello, self()})
{:get, :hello, #PID<0.41.0>}
iex> flush()
nil
:ok
一開始,程序映射沒有任何鍵,因此傳送 :get
訊息,然後清除目前的程序收件匣,會傳回 nil
。讓我們傳送 :put
訊息,再試一次
iex> send(pid, {:put, :hello, :world})
{:put, :hello, :world}
iex> send(pid, {:get, :hello, self()})
{:get, :hello, #PID<0.41.0>}
iex> flush()
:world
:ok
注意這個程序如何保持狀態,而且我們可以透過傳送程序訊息來取得並更新這個狀態。事實上,任何知道上述 pid
的程序都能傳送訊息給它並操作狀態。
也可以註冊 pid
,給它一個名稱,並允許任何知道該名稱的人傳送訊息給它
iex> Process.register(pid, :kv)
true
iex> send(:kv, {:get, :hello, self()})
{:get, :hello, #PID<0.41.0>}
iex> flush()
:world
:ok
使用程序來維護狀態和名稱註冊是 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
也可以提供 :name
選項給 Agent.start_link/2
,它會自動註冊。除了代理之外,Elixir 還提供用於建置通用伺服器(稱為 GenServer
)、登錄檔等的 API,這些全部都由底層的程序提供支援。這些,連同監督樹,將在 "Mix and OTP 指南" 中更詳細地探討,該指南將從頭到尾建置一個完整的 Elixir 應用程式。
現在,讓我們繼續探索 Elixir 中的 I/O 世界。