elixir官方入門教程 進程

#進程編程

1. `spawn`
2. `send`和`receive`
3. 連接
4. 任務
5. 狀態

在Elixir中,全部代碼都運行在進程內。進程相互獨立,併發地運行,經過傳送信息來交流。進程不是Elixir中惟一的併發基礎,但它意味着可以構建分佈式的,可容錯的程序。服務器

Elixir中的進程不能和操做系統中的進程搞混。Elixir中的進程在內存和CPU佔用上是極致的輕量級(不像其餘編程語言中的線程)。所以,同時運行數萬甚至數十萬的進程也就不足爲奇。併發

本章,咱們將學習用於生成進程的基礎結構,還有在進程間收發信息。app

#spawnasync

生成進程的基礎機制就是已經自動導入了的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

在咱們可以收發信息後,進程會變得有趣得多.

#sendreceive

咱們能夠用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/1spawn_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/1spawn_link/1不一樣的是,咱們用Task.start/1Task.start_link/1時會返回{:ok, pid},而不是隻有PID.這使得任務能夠被用於管理者樹上.Task提供了諸如Task.async/1Task.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時,給定的keyvalue就被存儲了.

讓咱們試着運行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世界.

相關文章
相關標籤/搜索