#進程編程
1. `spawn` 2. `send`和`receive` 3. 連接 4. 任務 5. 狀態
在Elixir中,全部代碼都運行在進程內。進程相互獨立,併發地運行,經過傳送信息來交流。進程不是Elixir中惟一的併發基礎,但它意味着可以構建分佈式的,可容錯的程序。服務器
Elixir中的進程不能和操做系統中的進程搞混。Elixir中的進程在內存和CPU佔用上是極致的輕量級(不像其餘編程語言中的線程)。所以,同時運行數萬甚至數十萬的進程也就不足爲奇。併發
本章,咱們將學習用於生成進程的基礎結構,還有在進程間收發信息。app
#spawn
async
生成進程的基礎機制就是已經自動導入了的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:oop
iex> self() #PID<0.41.0> iex> Process.alive?(self()) true
在咱們可以收發信息後,進程會變得有趣得多.
#send
和receive
咱們能夠用send/2
發送信息給進程,並用receiver/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>"
當在用戶界面中時,你會發現flush/0
助手很是有用.它會刷新並打印郵箱中的全部信息.
iex> send self(), :hello :hello iex> flush() :hello :ok
#連接
Elixir中最經常使用的生成方式就是spawn_link/1
.在咱們展現spawn_link/1
的例子以前,先看看當進程失敗時會發生什麼:
iex> spawn fn -> raise "oops" end #PID<0.58.0> [error] Process #PID<0.58.00> raised an exception ** (RuntimeError) oops :erlang.apply/2
僅僅是記錄了錯誤,進程依然可以被生成.這是由於進程之間是獨立的.若是咱們但願一個進程的失敗影響到其它進程,咱們就須要連接它們.使用spawn_link/1
:
iex> spawn_link fn -> raise "oops" end #PID<0.41.0> ** (EXIT from #PID<0.41.0>) an exception was raised: ** (RuntimeError) oops :erlang.apply/2
當殼中發生了一個錯誤,殼會自動捕獲這個錯誤並以良好的格式展現出來.爲了理解咱們的代碼中究竟發生了什麼,讓咱們在文件中來使用spawn_link/1
並運行它:
# spawn.exs spawn_link fn -> raise "oops" end receive do :hello -> "let's wait until the process fails" end
$ elixir spawn.exs ** (EXIT from #PID<0.47.0>) an exception was raised: ** (RuntimeError) oops spawn.exs:1: anonymous fn/0 in :elixir_compiler_0.__FILE__/1
這一次進程失敗了而且關閉了它所連接的父進程.咱們也能夠經過調用Process.link/1
來手工連接進程.咱們建議你查看一下Process
模塊,其中有進程的其餘功能.
進程和連接在建立可容錯系統中扮演着重要角色.在Elixir應用中,咱們常常將進程和管理者連接起來,管理者的做用是監督這塊區域中進程的生死.進程間是獨立的且默認不分享任何東西,因此它們不會毀壞或影響其它進程.
不一樣於其它語言要求咱們捕捉/處理異常,在Elixir中咱們能夠任由進程失敗,由於咱們指望管理者能合適地重啓咱們的系統."快速失敗"是編寫Elixir軟件時的一條守則.
spawn/1
和spawn_link/1
是Elixir裏用於建立進程的最原始的方法.儘管咱們目前只用過它們,但大多數時候咱們將抽象地在它們的上層操做.讓咱們來看看最經常使用的方式--任務.
#任務
任務創建在生成函數的上層,提供了更好的錯誤報告的檢討機制:
iex(1)> 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 (elixir) lib/task/supervised.ex:74: Task.Supervised.do_apply/2 (stdlib) proc_lib.erl:239: :proc_lib.init_p_do_apply/3 Function: #Function<20.90072148/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
函數等待着信息,而且對每一個信息作出合適的反應.當匹配到:get
信息時,它會反饋給調用者一個信息並再次調用loop/1
,等待新的信息.當:put
信息以新版本的映射調用了loop/1
時,給定的key
和value
就被存儲了.
讓咱們試着運行iex kv.exs
:
iex> {:ok, pid} = KV.start_link #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提供了代理,它是狀態的一種簡單抽象:
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_link/2
能夠設置:name
選項,而且會自動註冊.除了代理,Elixir還提供了用於建立通用服務器(GenServer),任務等等的API,它們的底層都是由進程支持的.咱們將在Mix和OTP入門中沿着管理者樹仔細探索,同時從頭至尾建立一個完整的Elixir應用.
接下來讓咱們開始探索Elixir中的I/O世界.