なるほど Erlang プロセス
初稿:2017-08-27
なるほど Erlang プロセス
なるほど Unix プロセス ― Ruby で学ぶ Unix の基礎という本があります。
この本は私にとって謎が多く触れにくかった Unix プロセスというものを、 Ruby からのプロセス操作を通じて馴染みのあるものにしてくれました。
今回はみなさんにとっても謎の多い ( かもしれない ) Erlang プロセスと、それを利用したプログラミングというものを、 Ruby に少し似た Elixir からの操作を通じてより良く理解してみましょう。
オンライン上でコンパイル・実行ができるサービス wandbox 上では Elixir が動作するため、Elixir を手元の PC にインストールしなくてもコードを編集、実行できます。今回のコードへのリンクを貼っておくのでもし興味があればコードを書き換えてオンラインで試してみてください。( 書き換えて実行しても他の人やリンクには影響が出ないので安心してください )
Erlang とは
「Rubyist のための他言語探訪 【第 10 回】 Erlang」によい記事があるので省略します。
Elixir とは
「書籍紹介「プログラミング Elixir」」によい記事があるので省略します。
Erlang プロセスは ID を持っている
Erlang プロセスは、 ID を持っています。 PID と呼ばれています。 self() という関数で自身の PID を返します。
以下のプログラムでは Ruby の p に相当する Elixir の関数 IO.inspect を使って self() の値を表示しています。
コード
Erlang プロセスは別のプロセスを作れる
Erlang プロセスは、別の Erlang プロセスを作ることができます。 spawn という関数を利用します。
Ruby で lambda を -> do … end や -> x do … end と書けるように、
Elixir では無名関数を fn -> … end や fn x -> … end と書けます。
spawn は引数に無名関数を取り、作成した Erlang プロセスの上でその関数を実行します。
ですから、以下のプログラムではメインの Erlang プロセスの PID と、 spawn で生成した Erlang プロセスでの PID が異なっています。
コード
Erlang プロセスは別の Erlang プロセスとやりとりできる
Erlang プロセスは、別の Erlang プロセスとの間でやりとりができます。プロセスとプロセスの間でやりとりする値のことはメッセージと呼ばれています。
あるプロセスから別のプロセスへメッセージを送るには send という関数を利用します。send の引数は送り先の PID と送りたいメッセージです。
プロセスへと送られてきたメッセージを取り出して読むには receive という関数を利用します。receive の do … end の中でメッセージを受けとることができます。
以下のプログラムでは Ruby の Object#inspect に相当する Elixir の関数 inspect と Ruby の puts に相当する Elixir の関数 IO.puts を使って、送られてきたメッセージの値を表示しています。
コード
Erlang プロセスはメッセージボックスを持つ
send でメッセージが送られてきたとき、受け手のプロセスでは明示的な処理は不要です。もちろんメッセージを 取り出して読む には先ほどの例のように receive を使わなければいけませんが、メッセージを 受けとる には何も必要ありません。全てのプロセスは、プロセスと一対一で結びついたキューを持っており、プロセスへ送られたメッセージはそのキューへと蓄積されます。この、プロセスに結びついたメッセージを格納するためのキューのことはメッセージボックスと呼ばれています。
以下のプログラムではプロセスの状態を調べられる Process.info を使って、プロセスのメッセージボックスの内容を表示しています。
コード
Erlang プロセスは並列に動ける
Erlang プロセスは並列に動作します。ハードウェアによる限りはありますが、プロセスそれぞれが同時に別の計算を行えるということです。
ある処理を 1 つだけ実行したときと、複数 ( 今回は 2 つ ) 実行したときの、結果が得られるまでの時間を比較して検証しましょう。例えば 2 つ並列に動かして、1 つ動かしたときの時間 * 2 より小さいなら、並列に動いているといえるでしょう。
以下のプログラムでは Ruby の sleep のように処理をスリープさせられる Elixir の関数 Process.sleep を使って、処理に 5 秒かかるようにしています。時間は Ruby の DateTime.now に似た Elixir の DateTime.utc_now で測ることにしました。
また、 1 回の receive で受けとれるメッセージは常に 1 つなので、ここでは 2 つのメッセージを受けとるため 2 回 receive しています。
コード
直列だと約 10 秒 (5 秒 * 2 回 ) かかって、並列だとほぼ 5 秒で終わっていますね。
Erlang プロセスは軽量
Erlang プロセスを作るのには、プロセスのヒープ領域込みで 2.5k バイト程度しか要しません。この記事のここまでの文字を UTF-8 として計算すると 7.8k バイトであるようなので、これでプロセス 3 つ分作れてしまうようです。
Ruby の Enumerable#reduce に似た Eliixr の関数 Enum.reduce を使って 10 万プロセスを畳み込み、生成と処理にかかる時間を計測しましょう。また、 Elixir からは Erlang の関数を直接呼び出せるので、 Erlang の関数 :erlang.memory(:total) で 10 万プロセスが生きているときのメモリ使用量も計測しましょう。
コード
プロセスを 10 万個生成して 1 ずつ足したので数値が計算結果が 10 万になっており、そのときの生成と実行にかかった時間は 4 秒程度、メモリ使用量は約 2.8 G バイトだったことがわかりますね。
Erlang プロセス同士のかかわり
ここまでは Erlang プロセス自身の性質を見てきました。ここからは Erlang プロセス同士の関係に関する性質を見ていきましょう。
プロセスを作り、そのプロセス上でエラーを起こしても、元のプロセスでは何も検知しません。
Ruby の raise に似た、 Elixir の raise でエラーを起こしてみましょう。
コード
エラーログはコンソールに出力されているものの、処理は正常に終わって done が表示されています。
Erlang プロセスは link できる
Erlang プロセス同士を link する方法があります。プロセス同士を繋げると、片方のプロセスで異常が起きたとき、もう一方へと知らせてくれます。
Elixir でプロセスを生成してすぐ link するには spawn_link という関数を使います。
コード
先程とは異なり done がコンソールに表示されていませんね。
生成してリンクしたプロセスにてエラーが発生、そのエラーが元のプロセスへ伝えられ、元のプロセスでもエラーハンドリングしていないため、元のプロセスもエラーになりました。
Erlang プロセスのエラーハンドリング
エラーを知らせてくれるのは便利ですけれども、エラーハンドリングしないと自分もエラーになってしまうのは不便ですね。
ある Erlang プロセスから別の Erlang プロセスへエラーを伝えるのは、特別なメッセージを送ることで行われています。そのメッセージの名前を exit シグナル といいます。ErlangVM には exit シグナル を通常のメッセージとして受け付ける仕組みがあるので、それを利用してエラーハンドリングします。
あるプロセスに Process.flag(:trap_exit, true) と書くと exit シグナル を通常のメッセージとして扱えるようになります。
コード
exit シグナルを通常のメッセージとして受信し、その後 done になっていますね。
Erlang プロセスを link するとどう嬉しいのか
こうしてプロセスを link しておくことにはどのような意味があるのでしょうか。この記事の最後に紹介している、『すごい Erlang ゆかいに学ぼう!』という本の「第 12 章 - エラーとプロセス (P151)」 には以下のように記述がありました。
もしエラーのあるプロセスがクラッシュしたけれど、それに依存しているプロセスが動き続けているとしたら、それら依存プロセスすべては依存先がなくなったことに対処しなければならなくなります。
link しておけば処理を実装するプログラマが考えなければいけない状態が一つ減ります。
また、 link したプロセスが死んだことをすぐに検知できると、時間をおかずに新しいプロセスを作りなおすことができます。エラー検知/再開を素早く行えると、一部の処理で不具合が起きても全体の動作には影響をほぼ与えずに復元することができ、全体の安定動作向上に寄与します。
Erlang プロセスは monitor できる
Erlang プロセス同士を link するのではなく、片方がもう片方を見ておく方法があります。 monitor といいます。先程の link は link 元と link 先が対等の立場でしたが、 monitor は monitor 元と monitor 先で立場が異なります。
Elixir でプロセスを生成してすぐ monitor するには spawn_monitor という関数を使います。
コード
link の際とは異なり Process.flag(:trap_exit, true) を使っていないプロセスでも受け取れていることに注意してください。exit シグナル ではない、単なるメッセージが送られてきます。
Erlang プロセスを monitor するとどう嬉しいのか
こうしてプロセスを monitor しておくことにはどのような意味があるのでしょうか。『すごい Erlang ゆかいに学ぼう!』「第 12 章 - エラーとプロセス (P158)」 には以下のように記述がありました。
モニターは、プロセスが下位のプロセスで何が起きているかを知りたいけれど、お互いが致命的な影響を及ぼしてほしくないときに便利です。( 略 ) 他のプロセスで何が起きているかを知る必要があるライブラリを書くときに活躍します。
私があまり monitor を使いこなしていないせいか、 monitor がバチッとハマりそうな例はうまく思いつきませんでした。すみません m(_ _)m みなさんでよい例を知っていたり、おもいついたらブログなどに書いていただけると嬉しいです。
Erlang プロセス同士の結びつきまとめ
以上のように、プロセス同士の結びつきの強度に応じていくつかの方法が提供されています。
- A が B を link した場合、 A がエラーになったら B へ exit シグナル が行く。 B がエラーになったら A へ exit シグナル が行く。
- A が B を monitor した場合、 A がエラーになっても B は影響を受けない。 B がエラーになったら A へ通常のメッセージが行く。
- それ以外の場合、他のプロセスで何が起きようと影響を受けない
壊れやすいタイマー
さてここまでプロセスの性質やプロセスのインタラクションについて説明してきたので、これらを組み合わせて簡単なアプリケーションを作ってみましょう。
壊れやすいタイマーというものを考えてみます。毎秒時刻を出力し、 30% の割合で壊れてしまうタイマーを考えましょう。
Ruby の module に似た、 Elixir の defmodule でモジュールを定義、Ruby の def に似た、 Elixir の def で関数を定義します。また Elixir には while のようなループがなく、ループは関数の中で自身の関数を呼び出す、いわゆる再帰で表現します。
コード
ここまでは意図通りに動くようです。とはいえ、壊れてしまいタイマーが動かなくなると困るので、タイマーが壊れたのを検知してすぐに新しいタイマーを起動する見張り役のプロセスを作り、タイマーを安定動作させることを目指します。
コード
壊れやすいタイマーと、見張り役を組み合わせることで多くの時間にはきちんと動くタイマーを作ることができましたね。
このコードではランダムで表現した「壊れやすい」部分というのは現実的なプログラミングだとどの部分になるでしょうか? 私は例えば TCP コネクションがそうだと考えています。 TCP コネクションは相手先の都合やネットワークでいつ切れるかわかりません。 HTTP サーバーや Websocket サーバーはこういった特色を持ちます。そして複数の TCP コネクションを持っているサーバーが、 1 つの TCP コネクションのエラーの悪影響を受けることを避けたいですよね。こういったケースには ErlangVM のプロセスの性質が生かされます。
Erlang プロセスを扱うライブラリ Erlang//OTP
これまで挙げたようなプロセスの協調動作を駆使するのが ErlangVM のプログラミングの面白く難しいところですが、これらのプリミティブな性質を直接使うのではなく、便利に利用するためのライブラリ OTP というものが Erlang に標準添付されています。
説明ではわかりやすさのために、spawn や link や _monitor_を直接利用していましたが、私がこれまで眺めたことのある Erlang/Elixir ライブラリたちではそれらはほとんど使われず、 OTP を使うものが多かったです。
プロダクションで利用するコードには OTP を利用しましょう。
最後に
私が最初に Erlang を学びはじめたとき、よくわからなかったのは「ErlangVM でのプログラミングは Ruby に比べてどういう利点があるのだろう?」というところでした。学んでいくうちに、それは Erlang プロセス上の処理について着目していたから、利点がよくわからなかったのだという感想にいたっています ( 今のところ )。 ErlangVM プログラミングは、プロセス上の処理 より、 プロセスとプロセスのつながり方 をどうデザインするかいう視点で捉えてみると利点がよく見えてくる気がしました。
Erlang プロセスや OTP について深く知りたければ私は『すごい Erlang ゆかいに学ぼう! 』という本をおすすめします。この本、特に「第 12 章 - エラーとプロセス」を読んで得た知識を元にこの記事に書きました。
また Elixir に興味が湧いた方には 『プログラミングElixir』をおすすめします。 この本にも当然 Erlang プロセスや OTP のことがしっかりと書かれているので Rubyist の方はこちらの本から読み始める方がとっつきやすいかもしれません。 私は最初この本から入りました。
著者について
ヽ(´・肉・`)ノ @niku_name
札幌に住んでいて、お仕事や趣味で Ruby を書いています。だいたい毎週木曜日に開催されているサッポロビームという ErlangVM について話す集まりに参加しています。たまに RubySapporo.beam というイベント ( #1, #2 ) を開催しています。