11-進程
=======
Elixir中,所有代碼都在進程中執行。進程彼此獨立,一個接一個并發執行,彼此通過消息傳遞來溝通。
進程不僅僅是Elixir中并發的基礎,也是Elixir創建分布式、高容錯程序的本質。
Elixir的進程和操作系統中的進程不可混為一談。
Elixir的進程,在CPU和內存使用上,是極度輕量級的(不同于其它語言中的線程)。
因此,同時運行著數十萬、百萬個進程也并不是罕見的事。
本章將講解如何派生新進程,以及在進程間如何發送和接受消息等基本知識。
## 11.1-進程派生
派生(spawning)一個新進程的方法是使用自動導入(kernel函數)的```spawn/1```函數:
```elixir
iex> spawn fn -> 1 + 2 end
#PID<0.43.0>
```
函數```spawn/1```接收一個_函數_作為參數,在其派生出的進程中執行這個函數。
注意spawn/1返回一個PID(進程標識)。在這個時候,這個派生的進程很可能已經結束。
派生的進程執行完函數后便會結束:
```elixir
iex> pid = spawn fn -> 1 + 2 end
#PID<0.44.0>
iex> Process.alive?(pid)
false
```
>你可能會得到與例子中不一樣的PID
用```self/0```函數獲取當前進程的PID:
```elixir
iex> self()
#PID<0.41.0>
iex> Process.alive?(self())
true
```
>注:上文調用```self/0```加了括號。
但是如前文所說,在不引起誤解的情況下,可以省略括號而只寫```self```
可以發送和接收消息,讓進程變得越來越有趣。
## 11.2-發送和接收
使用```send/2```函數發送消息,用```receive/1```接收消息:
```elixir
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```。
當然也可以給子句加上衛兵表達式。
如果找不到匹配的消息,當前進程將一直等待,知道下一條信息到達。但是可以設置一個超時時間:
```elixir
iex> receive do
...> {:hello, msg} -> msg
...> after
...> 1_000 -> "nothing after 1s"
...> end
"nothing after 1s"
```
超時時間設為0表示你知道當前郵箱內肯定有郵件存在,很自信,因此設了這個極短的超時時間。
把以上概念綜合起來,演示進程間發送消息:
```elixir
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```很有用。它清空緩沖區,打印進程郵箱中的所有消息:
```elixir
iex> send self(), :hello
:hello
iex> flush()
:hello
:ok
```
## 11.3-鏈接
Elixir中最常用的進程派生方式是通過函數```spawn_link/1```。
在舉例子講解```spawn_link/1```之前,來看看如果一個進程失敗了會發生什么:
```elixir
iex> spawn fn -> raise "oops" end
#PID<0.58.0>
```
。。。啥也沒發生。這時因為進程都是互不干擾的。如果我們希望一個進程中發生失敗可以被另一個進程知道,我們需要鏈接它們。
使用```spawn_link/1```函數,例子:
```elixir
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```并且執行和觀察它:
```elixir
# spawn.exs
spawn_link fn -> raise "oops" end
receive do
:hello -> "let's wait until the process fails"
end
```
這次,該進程在失敗時把它的父進程也弄停止了,因為它們是鏈接的。<br/>
手動鏈接進程:```Process.link/1```。
建議可以多看看[Process模塊](http://elixir-lang.org/docs/stable/elixir/Process.html),里面包含很多常用的進程操作函數。
進程和鏈接在創建能高容錯系統時扮演重要角色。在Elixir程序中,我們經常把進程鏈接到某“管理者”上。
由這個角色負責檢測失敗進程,并且創建新進程取代之。因為進程間獨立,默認情況下不共享任何東西。
而且當一個進程失敗了,也不會影響其它進程。
因此這種形式(進程鏈接到“管理者”角色)是唯一的實現方法。
其它語言通常需要我們來try-catch異常,而在Elixir中我們對此無所謂,放手任進程掛掉。
因為我們希望“管理者”會以更合適的方式重啟系統。
“要死你就快一點”是Elixir軟件開發的通用哲學。
在講下一章之前,讓我們來看一個Elixir中常見的創建進程的情形。
## 11.4-狀態
目前為止我們還沒有怎么談到狀態。但是,只要你創建程序,就需要狀態。
例如,保存程序的配置信息,或者分析一個文件先把它保存在內存里。
你怎么存儲狀態?
進程就是(最常見的)答案。我們可以寫無限循環的進程,保存一個狀態,然后通過收發信息來告知或改變該狀態。
例如,寫一個模塊文件,用來創建一個提供k-v倉儲服務的進程:
```elixir
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更新了)再次調用自身。
執行一下試試:
```elixir
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```消息:
```elixir
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,給它一個名稱。這使得人人都知道它的名字,并通過名字來向它發送消息:
```elixir
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](http://elixir-lang.org/docs/stable/elixir/Agent.html)就是一種維護狀態的簡單的抽象實現:
```elixir
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手冊里找到詳細說明。