Elixir入門 11: プロセス
Tags: elixir, webdev, tutorial, programming
Published: 2018-11-27 01:13:14

---
title: Elixir入門 11: プロセス
published: true
description: Elixir公式サイトの許諾を得て「Processes」の解説にもとづき、加筆補正を加えて、Elixirにおけるプロセスのつくり方やプロセス間のメッセージのやり取りの仕方についてご説明します。
tags: #elixir, #webdev, #tutorial, #programming
---

本稿はElixir公式サイトの許諾を得て「[Processes](https://elixir-lang.org/getting-started/processes.html)」の解説にもとづき、加筆補正を加えて、Elixirにおけるプロセスのつくり方やプロセス間のメッセージのやり取りの仕方についてご説明します。

Elixirではすべてのコードがプロセスの中で動きます。プロセスは互いに切り離され、並行して働き、メッセージを受け渡して通信します。プロセスはElixirの並行処理の基礎となるだけでなく、分散性と耐障害性([フォールトトレランス](http://e-words.jp/w/%E3%83%95%E3%82%A9%E3%83%BC%E3%83%AB%E3%83%88%E3%83%88%E3%83%AC%E3%83%A9%E3%83%B3%E3%82%B9.html))に優れたプログラムの構築に役立つのです。

Elixirのプロセスを、オペレーティングシステムのプロセスと混同しないでください。Elixirのプロセスは、メモリやCPUに対してきわめて軽量です。他の多くのプログラミング言語のスレッドとは異なります。同時に数万から数十万のプロセスを実行することもめずらしくはありません。


# spawn
新しいプロセスをつくるには、[`spawn/1`関数](https://hexdocs.pm/elixir/Kernel.html#spawn/1)の呼び出しが基本となります。引数は別のプロセスで実行する関数で、戻り値はアプリケーションにおける一意のプロセス識別子PIDです(識別番号は環境により変わります)。つぎのコードで生成されたプロセスは、与えられた関数を実行したら終了します。

```elixir
iex> spawn fn -> 1 + 2 end
#PID<0.87.0>
```

現在のプロセスのPIDを調べるのは[`self/0`](https://hexdocs.pm/elixir/Kernel.html#self/0)関数です。そのPIDを[`Process.alive?/1`](https://elixir-lang.org/getting-started/processes.html)に渡して呼び出せば、プロセスが動いているかどうか確かめられます。

```elixir
iex> pid = self()
#PID<0.84.0>
iex> Process.alive?(pid)
true
```

[`spawn/3`関数](https://hexdocs.pm/elixir/Kernel.html#spawn/3)を使えば、モジュールの関数からプロセスがつくれます。引数はモジュールと関数、および渡す引数のリストです。なお、第2引数の関数はアトムで与えてください。

```elixir
defmodule Example do
  def add(a, b) do
    IO.puts(a + b)
  end
end
```

```elixir
iex> spawn(Example, :add, [2, 3])
5
#PID<0.90.0>
```


# 送信と受信
[`send/2`](https://hexdocs.pm/elixir/Kernel.html#send/2)でメッセージを送ると、[`receive/1`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#receive/1)で受け取れます。

送られたメッセージはプロセスメールボックスに納められます。すると、[`receive/1`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#receive/1)ブロックが現在のプロセスメールボックスを探し、パターンにマッチしたメッセージを受け取るのです。`receive/1`には`case/2`と同じように、ガードと複数の句が含められます。

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

パターンに合うメッセージがメールボックスになければ、現在のプロセスはマッチするメッセージが届くまで待ちます。この場合、タイムアウトも定められます。すでにメッセージがあると想定されるなら、値は0にしても構いません。なお、数値には桁区切りのためにアンダースコア`_`を加えることができます。

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

ふたつのプロセス間でメッセージを送ってみましょう。つぎの送信のプロセスは、`receive/1`ブロックが受け取って値を返すと、他の処理はありませんので終了します。なお、[`inspect/2`関数](https://hexdocs.pm/elixir/Kernel.html#inspect/2)(第2引数のデフォルト`[]`)は、引数のデータを内部的な文字列表現に変え、おもに出力に用いられます。

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

シェルでメールボックス内のすべてのメッセージを出力して空にするには、ヘルパー関数[`flush/0`](https://hexdocs.pm/iex/IEx.Helpers.html#flush/0)をお使いください。

```elixir
iex> send self(), :hello
:hello
iex> send self(), :world
:world
iex> flush()
:hello:world
:ok
iex> flush()
:ok
```

モジュールの関数からつくったプロセスにもメッセージが送れます。ただし、関数がメッセージを受け取って処理をすると終了するため、つぎに送るメッセージは受け取られません。


```elixir
defmodule Example do
  def listen do
    receive do
      {:hello, msg} -> IO.puts(msg)
    end
  end
end
```

```elixir
iex> pid = spawn(Example, :listen, [])
#PID<0.90.0>
iex> send pid, {:hello, "world"}
world
{:hello, "world"}
iex> send pid, {:hello, "tokyo"}
{:hello, "tokyo"}
```

関数を再帰呼び出しすれば処理は終わらず、何度でもメッセージが送れます。

```elixir
defmodule Example do
  def listen do
    receive do
      {:hello, msg} -> IO.puts(msg)
    end
    listen  #再帰呼び出し
  end
end
```

```elixir
iex> send pid, {:hello, "world"}
world
{:hello, "world"}
iex> send pid, {:hello, "tokyo"}
tokyo
{:hello, "tokyo"}
```



# リンク
Elixirでプロセスをつくるとき、多くの場合リンクさせます。リンクしたプロセスを試す前に、`spawn/1`のプロセスが失敗したときどうなるかみておきましょう。[`raise/1`](https://hexdocs.pm/elixir/Kernel.html#raise/1)で`RuntimeError`の例外を起こしても、エラーが記録されるだけで、親プロセスは動いています。それは、プロセスが互いに切り離されているからです。

```elixir
iex> spawn fn -> raise "oops" end#PID
<0.86.0>
iex>
[error] Process #PID<0.86.0> raised an exception
** (RuntimeError) oops
    (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
```

あるプロセスの失敗を他に伝えるには、それらをリンクしなければならないのです。そのためには、[`spawn_link/1`](https://hexdocs.pm/elixir/Kernel.html#spawn_link/1)を用いて現在のプロセスにリンクします。

つぎのコードでは、`spawn_link/1`で親プロセスのシェルにリンクしました。そのため、子プロセスの例外から、親のシェルが`EXIT`の通知を受け取ったのです。IExはシェルの終了を検知し、新たなセッションがはじまります。

```elixir
iex> self()
#PID<0.84.0>
iex> spawn_link fn -> raise "oops" end
** (EXIT from #PID<0.84.0>) shell process exited with reason: an exception was raised:
    ** (RuntimeError) oops
        (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6

[error] Process #PID<0.90.0> raised an exception
** (RuntimeError) oops
    (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
```

[`spawn_link/3`関数](https://hexdocs.pm/elixir/Kernel.html#spawn_link/3)を使うと、モジュールの関数からプロセスがリンクできます。引数はモジュールと関数、および渡す引数のリストです。[`exit/1`](https://hexdocs.pm/elixir/Kernel.html#exit/1)は、呼び出したプロセスを終了します。そのとき、引数が理由として示されます。

```elixir
defmodule Example do
  def explode, do: exit(:boom)
end
```

```elixir
iex> spawn(Example, :explode, [])
#PID<0.89.0>
iex> spawn_link(Example, :explode, [])
** (EXIT from #PID<0.87.0>) shell process exited with reason: :boom
```

リンクした現在のプロセスを落としたくない場合には、[`Process.flag/2`](https://hexdocs.pm/elixir/Process.html#flag/2)を用います。第1引数にフラグ`:trap_exit`、第2引数の値に`true`を与えると、終了が止められるのです。この関数はerlangの[`process_flag/2`](http://erlang.org/doc/man/erlang.html#process_flag-2)によりフラグを定めています(「[Receiving Exit Signals](http://erlang.org/doc/reference_manual/processes.html#receiving-exit-signals)」参照)。終了を止めると、`{:EXIT, from_pid, reason}`というタプルでメッセージが受け取られます。

```elixir
defmodule Example do
  def explode, do: exit(:boom)
  def run do
    Process.flag(:trap_exit, true)
    spawn_link(Example, :explode, [])
    receive do
      {:EXIT, from_pid, reason} -> IO.puts("Exit reason: #{reason}")
    end
  end
end
```

```elixir
iex> Example.run
Exit reason: boom
:ok
```

ふたつのプロセスをリンクさせるのでなく、情報を得たい場合があります。そういうときに用いるのが[`spawn_monitor/3`](https://hexdocs.pm/elixir/Kernel.html#spawn_monitor/3)で、戻り値はPIDと監視するプロセスの参照です。プロセスがクラッシュすると、メッセージを受け取ります。現在のプロセスは落ちませんし、終了を止める必要もありません。

```elixir
defmodule Example do
  def explode, do: exit(:boom)
  def run do
    {_pid, _ref} = spawn_monitor(Example, :explode, [])
    receive do
      {:DOWN, _ref, :process, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
    end
  end
end
```

```elixir
iex> Example.run
Exit reason: boom
:ok
```

リンクは[`Process.link/1`](https://hexdocs.pm/elixir/Process.html#link/1)を呼び出して定めることもできます。[`Process`](https://hexdocs.pm/elixir/Process.html)モジュールには、そのほかにもさまざまな機能が備わっています。

プロセスとリンクは、フォールトトレランスに優れたシステムを構築するためにも重要な役割を果たします。Elixirのプロセスは互いに切り離されており、デフォルトでは何も共有しません。したがって、ひとつのプロセスの失敗が他のプロセスをクラッシュさせたり、悪影響を及ぼすことはないのです。

けれど、プロセスをリンクすると、障害が起きたときの関係がつくれます。プロセスがよくリンクされるのはスーパーバイザーです。スーパーバイザーはプロセスが落ちたことを検出し、代わりの新たなプロセスを開始できます。

他の言語では例外を補足して処理しなければなりません。Elixirはスーパーバイザーがシステムを適切に再起動できるので、プロセスが失敗したままで構いません。「早く失敗させる」というのは、Elixirでソフトウェアを開発するときの一般的な考え方です。


# Task
`spawn`関数にもとづいて構築された`Task`は、よりよいエラーレポートとイントロスペクション(introspection)の機能を提供します。`spawn/1`や`spawn_link/1`の替わりに[`Task.start/1`](https://hexdocs.pm/elixir/Task.html#start/1)と[`Task.start_link/1`](https://hexdocs.pm/elixir/Task.html#start_link/1)を使うと、戻り値は単にPIDではなくタプル`{：ok、pid}`になります。これでタスクが監視ツリーで扱えるようになるのです。エラーレポートも詳しくなります。

```elixir
iex> Task.start fn -> raise "oops" end
{:ok, #PID<0.91.0>}
iex>
[error] Task #PID<0.91.0> started from #PID<0.87.0> terminating
** (RuntimeError) oops
    (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
    (elixir) lib/task/supervised.ex:88: 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: []
```

[`Task.async/3`](https://hexdocs.pm/elixir/Task.html#async/3)はバックグラウンドで関数を実行します。その戻り値を[`Task.await/2`](https://hexdocs.pm/elixir/Task.html#await/2)に渡せば、結果の値がえられるのです(第2引数のタイムアウトはデフォルト値5000)。アプリケーションの実行は止めずに、負荷の高い処理をするときに役立ちます。なお、[`:timer.sleep/1`](http://erlang.org/doc/man/timer.html#sleep-1)は引数のミリ秒間、処理を一時停止するerlangの関数です。

```elixir
defmodule Math do
  def hypot(x, y) do
    :timer.sleep(3000)
    :math.sqrt(x * x + y * y)
  end
end
```

```elixir
iex> task = Task.async(Math, :hypot, [3, 4])
%Task{
  owner: #PID<0.87.0>,
  pid: #PID<0.94.0>,
  ref: #Reference<0.4236645349.3512991745.24823>
}
iex> Math.hypot(5, 12)
13.0  #<- 3秒後
iex> Task.await(task)
5.0
```


# 状態
構築するアプリケーションに、状態をもたせたい場合があります。たとえば、アプリケーションの設定を保持したり、ファイルを解析してメモリに読み込みたいときなどです。

状態は一般にプロセスにもたせます。プロセスをつくってループさせ、状態を保持し、メッセージをやり取りするのです。ここで、キーと値の組みが納められる新たなプロセスをつくってみましょう。

初期化の関数(`start_link`)は、プロセスを`Task.start_link/1`でつくり、プライベートの関数(`loop`)に空のマップを渡します。`receive/1`ブロックは、メッセージのキーが`:get`のときは、[`send/2`](https://hexdocs.pm/elixir/Kernel.html#send/2)でメッセージを送ります。そして、`Map.get/3`で得たキーの値を返すのです。キーが`:put`なら、`Map.put/3`でマップにキーと値を加えます。いずれも関数を再帰呼び出しすることにより、処理を続けていることにご注目ください。

```elixir
defmodule KeyValue 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
```

モジュールの初期化の関数(`KeyValue.start_link`)を呼び出したときは、まだマップは空です。そのため、`:get`メッセージを送っても値は得られません。現在のプロセスの受信トレイには何も入っていないのです。

```elixir
iex> {:ok, pid} = KeyValue.start_link
{:ok, #PID<0.91.0>}
iex> send(pid, {:get, :hello, self()})
{:get, :hello, #PID<0.87.0>}
iex> flush
nil
:ok
```

プロセスに`:put`メッセージを送ると状態が更新され、キーと値が加わります。そのあとは、`:get`メッセージでキーの値も得られるでしょう。PIDを知っていれば、別のプロセスであっても、メッセージを送って状態は操作できます。

```elixir
iex> send pid, {:put, :hello, :world}
{:put, :hello, :world}
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.87.0>}
iex> flush
:world
:ok
```
[`Process.register/2`](https://hexdocs.pm/elixir/Process.html#register/2)を使えば、PIDに名前をつけて登録できます。どのプロセスからでも、その名前でメッセージが送れるようになるのです。

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

状態と名前をプロセスに登録して保持することは、Elixirアプリケーションでたびたび使われる手法です。もっともほとんどの場合、ご紹介した手動で処理するコードは書きません。Elixirには多くの抽象的なやり方が予め備わっています。たとえば、状態を抽象化したのが`Agent`です。


# Agent
`Agent`は、状態が保持されているバックグラウンドのプロセスを抽象化です。アプリケーションとノードの他のプロセスからアクセスできます。[`Agent.start_link/2`](https://hexdocs.pm/elixir/Agent.html#start_link/2)は現在のプロセスにリンクされた`Agent`をはじめます(第2引数のオプションはデフォルト値`[]`)。戻り値のタプル`{:ok, pid}`の第2要素が`Agent`の状態への参照です。

[`Agent.update/3`](https://hexdocs.pm/elixir/Agent.html#update/3)により、第1引数の`Agent`の状態を第2引数の関数で更新できます(第3引数のタイムアウトはデフォルト値5000ミリ秒)。そして、`Agentから値を取り出すのが`[`Agent.get/3`](https://hexdocs.pm/elixir/Agent.html#get/3)です。第1引数の`Agent`から得た値を、第2引数の関数で処理して返します(第3引数のタイムアウトはデフォルト値5000ミリ秒)。

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

```elixir
iex> {:ok, agent} = Agent.start_link(fn -> [1, 2, 3] end)
{:ok, #PID<0.105.0>}
iex> Agent.update(agent, fn state -> state ++ [4, 5] end)
:ok
iex> Agent.get(agent, &(&1 -- [2, 4]))
[1, 3, 5]
```


#### Elixir入門もくじ
- [Elixir入門 01: コードを書いて試してみる](https://dev.to/gumi/elixir-01--2585)
- [Elixir入門 02: 型の基本](https://dev.to/gumi/elixir-02--30n1)
- [Elixir入門 03: 演算子の基本](https://dev.to/gumi/elixir-03--33im)
- [Elixir入門 04: パターンマッチング](https://dev.to/gumi/elixir-04--1346)
- [Elixir入門 05: 条件 - case/cond/if](https://dev.to/gumi/elixir-05----casecondif-60o)
- [Elixir入門 06: バイナリと文字列および文字リスト](https://dev.to/gumi/elixir-06--35na)
- [Elixir入門 07: キーワードリストとマップ](https://dev.to/gumi/elixir-07--39hi)
- [Elixir入門 08: モジュールと関数](https://dev.to/gumi/elixir-08--1c4c)
- [Elixir入門 09: 再帰](https://dev.to/gumi/elixir-09--1a0p)
- [Elixir入門 10: EnumとStream](https://dev.to/gumi/elixir-10-enumstream-4fpb)
- Elixir入門 11: プロセス
- [Elixir入門 12: 入出力とファイルシステム](https://dev.to/gumi/elixir-12--4og6)
- [Elixir入門 13: aliasとrequireおよびimport](https://dev.to/gumi/elixir-13-aliasrequireimport-55c1)
- [Elixir入門 14: モジュールの属性](https://dev.to/gumi/elixir-14--3511)
- [Elixir入門 15: 構造体](https://dev.to/gumi/elixir-15--4f43)
- [Elixir入門 16: プロトコル](https://dev.to/gumi/elixir-16--lif)
- [Elixir入門 17: 内包表記](https://dev.to/gumi/elixir-17--5gci)
- [Elixir入門 18: シギル](https://dev.to/gumi/elixir-18--5791)
- [Elixir入門 19: tryとcatchおよびrescue](https://dev.to/gumi/elixir-19-trycatchrescue-50i8)
- [Elixir入門 20: 型の仕様とビヘイビア](https://dev.to/gumi/elixir-20--j50)
- [Elixir入門 21: デバッグ](https://dev.to/gumi/elixir-21--21a1)
- [Elixir入門 22: Erlangライブラリ](https://dev.to/gumi/elixir-22-erlang-2492)
- [Elixir入門 23: つぎのステップ](https://dev.to/gumi/elixir-23--50ik)

##### 番外
- [Elixir入門: Plugを使うには](https://dev.to/gumi/elixir-plug-40lb)