(この記事は Elixir (その2)とPhoenix Advent Calendar 2016 13日目の記事です)
7/29(土)、「Scala福岡2017」で、「Spark+Mahoutを使った機械学習型データマイニングによる高速レコメンドエンジン開発、活用」について、40分ほど、セッション枠でお話しました
上記はメッチャScalaネタなのに、唐突で何なんですが、私がElixirに惚れ込んだ要素として最も強いのは、
「高速・分散・並列・データ変換ドリブン」
を出自として備えていることと、それらを支えるテクノロジーである、
「メッセージパッシング・パターンマッチング・不変 x マルチコア活用・劣化しないマルチプロセス」
が、コーディングしてて楽しいことです
Elixirをフル活用して、上記したSpark+Mahoutのような機械学習BIエンジンを半年~1年以内に、そこそこ強めのAIを搭載した分散並列マイクロカーネルOS or 分散並列ビッグデータエンジンを2年以内に、それぞれ開発してみたいなぁ、なんて野望があります
Elixirのプリミティブなプロセス
さて本編です
「Elixirで弱々しいAIを作る#5:感情のコーディング」で、「Agent」という、状態を保持するためのバックグラウンドプロセスをサクッと作りましたが、今回扱うのは、よりプリミティブな、「プロセス生成」と「メッセージパッシング」です
このネタ、SlideShareに資料化して、ずっと前にアップしていたんですが、あまり人気が無い(笑)せいか、PVがそれほど伸びていなかったので、Qiitaでスライドを解説してみようかな、と思いました
ちなみにこのスライド、私が2番目に尊敬するプログラマ師匠である、Dave Thomasさんが書いた「プログラミングElixir」を読んでいて、Elixirはじめたばかりの私を「これは脱落させにかかっているのでは?...」と思わせたところから、他の人もそうなるのを避けるため、「分かりやすく書き直すとしたら、どう書くか?」から作成してみました
マルチプロセス関連の以下4つの記載が分かりにくく、コードを逐一分解しないと理解できない、といった感じでした
- P161~166
- 初っ端から、双方向メッセージパッシングしているから、send()とreceiveの関係が理解しにくい
- self()でPID特定したiexにsend()し、それをreceiveで受信するため、益々分かりにくい
- P186~189
- __ MODULE __が唐突に出てきて分かりにくい
- その他
- 各種クライアントサーバのサンプルアプリがトイ感高くて、実用イメージが湧きにくい
ついでに、リモートのElixirとモジュールを共有するための「nl()」の解説も足しました
P5:前準備
上記スライドのP5からスタートします(なお、SlideShareでのページ番号では無く、資料内のページ番号にて説明します)
スライドでは、Dockerを使った手順になっていますが、ローカルPC版で行いたい場合は、「Elixirで弱々しいAIを作る:#1MeCabで文章パース」を参考にしてください
ここはあまり解説することが無いので、Pass.say()のコーディング・実行まで終えたら、次に進んでください
defmodule Pass do
def say( message ¥¥ "こんにちは。" ), do: IO.puts( message )
end
P6:プロセス生成
Elixirプロセスは、spawn()に、「モジュール名」「関数名」「引数リスト」の3つを渡すことで生成できます
iex> spawn( Pass, :say, [] )
こんにちは。
#PID<0.1805.0>
プロセスとして生成すると、プロセスID(PID)が付与されます
なお今回は、カンタンなプロセス生成のため、引数リストが空ですが、マルチプロセスでの分散・並列処理を行う場合、リモートで処理した内容を元にspawn()することもあるため、これはより分散・並列処理を意識した別シリーズにて今後解説します
おまけ:Elixirでのプロセス構成について
実は、iex自体も、1つのプロセスであり、以下のようにPIDを確認できます
iex> self()
#PID<0.80.0>
他にも、IO.puts()やIO.inspect()で使う「IO」も、1つのプロセスで、「IO」含む様々なカーネルプロセス群を以下のように確認できます
iex> :erlang.group_leader()
#PID<0.33.0>
こうしたプロセスに共通するのは、「Elixirで弱々しいAIを作る#5:感情のコーディング」でも説明した通り、「生きているプロセスが状態を保持している」ということです
そして、その状態の取得や更新をしたい場合、プロセスとの通信、つまりメッセージパッシングによって解決します
つまり、iexやIOも、Feelingモジュールと同じ、状態の取得や更新を受け付ける「サーバプロセス」なのです
こうすることで、「状態」というシングルトンに対する競合更新を排除し、分散・並列処理をシンプルなブロッキング処理に落とし込んでいます(ここが興味深い方は、末尾の「余談その2」がオススメ)
さて、これらプロセスIDを、ビジュアルツールで見ることもできます
ただし、DockerでElixirを動かしている等の場合は、Docker自身が画面を持っていないため、リモートの画面を持つPCにポートフォワードする等の工夫が必要です(ohrdevさんの「リモートのElixirアプリをobserverでモニタリングする」の記事を参考にしてください)
iex> :observer.start
「Application」タブ内の「kernel」配下に、:erlang.group_leader()で確認できたPIDが見えます
その配下にいるプロセスも、OS好き...特にマークロカーネルOS好きにはたまらないプロセス構成をしています
なお、別タブを見てみると、こちらも色々と興味深いインディケータがありますが、このツール自体、マルチプロセス時の性能検証をするときに活用しますので、今後のコラムでも、たびたび出てきます
P8~9:単方向メッセージ受信を行うプロセスの生成(単発)
P8~9で、メッセージ受信を行います
defmodule Pass do
…
def confirm() do
receive do
{ true, message } ->
IO.puts( "受信メッセージ'#{message}'。" )
end
IO.puts( "------ confirm() end ------" )
end
…
メッセージ受信するサーバプロセスを起動します
iex> recompile()
iex> pid = spawn( Pass, :confirm, [] )
#PID<0.1768.0>
サーバプロセスにメッセージ送信してみます
iex> send( pid, { true, "hello" } )
受信メッセージ'hello'。
{true, "hello"}
------ confirm() end ------
メッセージ受信されました
しかし、もう1度送信すると、今度はメッセージ受信されません
iex> send( pid, { true, "hello" } )
{true, "hello"}
これは、受信すると、サーバプロセス(の関数)が終了してしまう造りになっているからです
P10:単方向メッセージ受信を行うプロセスの生成(再帰で繰り返し)
再帰による複数メッセージ受信を行います
defmodule Pass do
…
def hear() do
receive do
{ true, message } ->
IO.puts( "受信メッセージ'#{message}'。再度メッセージを受け取れます。" )
end
hear()
IO.puts( "------ hear() end ------" )
end
…
再帰でメッセージ受信するサーバプロセスを起動します
iex> recompile()
iex> pid = spawn( Pass, :hear, [] )
#PID<0.496.0>
サーバプロセスに繰り返しメッセージ送信してみます
iex> send( pid, { true, "hello" } )
受信メッセージ'hello'。再度メッセージを受け取れます。
{true, "world"}
iex> send( pid, { true, "hello" } )
受信メッセージ'hello'。再度メッセージを受け取れます。
{true, "world"}
P11:受信内容に応じたパターンマッチ
receiveの後に、1つ以上のパターンマッチを並べることで、「受信内容に応じた処理の仕分け」ができます
これは、「引数のパターンで、どの関数を呼び分けるか?」という、Elixirの最も強力なパターンマッチと全く同じことが、プロセス間通信でも使える、ということを意味しています
まずサーバプロセスを起動します
iex> recompile()
iex> pid = spawn( Pass, :hear, [] )
#PID<0.496.0>
この例では、サーバプロセスに、ステータスとしてtrueを送信したときは反応し、okを送信したときは反応しない、という「受信内容に応じた処理の仕分け」を試しています
iex> send( pid, { true, "hello" } )
受信メッセージ'hello'。再度メッセージを受け取れます。
{true, "world"}
iex> send( pid, { ok, "hello" } )
{ok, "world"}
今回は、「trueステータス時にあらゆるメッセージを受け取る」という、非常にカンタンなメッセージ受信パターンのみですが、実際の分散・並列処理では、ステータス/メッセージ共に、様々なパターンに分岐したり、メッセージ送受信により状態遷移を管理するような「ステートマシン」として構成したりします(こちらも別シリーズにて今後解説します)
P13~15:双方向メッセージパッシング
P13~15でコーディングする通り、receiveでメッセージ受信後に、送信元(iexを表すself())へとsend()することで、双方向のメッセージパッシングが可能となります
defmodule Pass do
…
def hear_after_say() do
receive do
{ sender_pid, message } ->
IO.puts( "受信メッセージ'#{message}'。返信をご確認ください。" )
send( sender_pid, { true, "これは'#{message}'の受信に対する返信です" } )
end
hear_after_say()
IO.puts( "------ hear_after_say() end ------" )
end
…
サーバプロセスを起動します
iex> recompile()
iex> pid = spawn( Pass, :hear_after_say, [] )
#PID<0.240.0>
サーバプロセスにメッセージ送信します
iex> send( pid, { self(), "Yo, Yo" } )
受信メッセージ'Yo, Yo'。返信をご確認ください。
{#PID<0.209.0>, "Yo, Yo"}
サーバプロセスから返信があるので、確認します
iex> receive do { true, message } -> IO.puts message end
「Yo, Yo」受信の返信
:ok
返信が確認できました
さて、ここまで9スライドも使って解説した内容が、「プログラミングElixir」のP163にサラっと1ページで書かれている内容なのです...
...うーん、Elixir初級者には、なかなかハードル高い
ここで脱落してしまうと、その後に書かれている本当に興味深いところ(以下)を見逃してしまうため、それはもったいなさ過ぎるなぁ...と思ったことも、スライドを作った動機ですね
- P167~170:Eixirは膨大なマルチプロセスを起動してもシーケンシャルに伸び、性能劣化しない
- P183~192:複数ノード(PC or Docker)での分散処理
- P193~232:OTP各種(サーバ、スーパーバイザー、アプリケーション)
- P233~239:「Task」と「Agent」
今回はここまで
次回に続きます
余談
今回のコラムに興味高い方は、「QNX」という軽量メッセージパッシングOSのことを調べると、メッチャ楽しいかも
QNXで培われている軽量化テクノロジーと同じものがElixir/Erlangで実現されていることに気付きます
既に絶版で残念なのですが、本も出ていて、これと上記:observer.startで起動したビジュアルアプリを見比べると、ニヤニヤが回避できなくなります
ちなみに現在のQNXは、車載搭載機として活用されており、iPhoneと車を接続する「Apple CarPlay」で使われているようです
他にも、携帯基地局端末として、リアルタイム処理を捌くなど、Elixirの元となっているErlangが、電話交換機におけるリアルタイム処理を捌いてきたのと、似たような経歴があります
年も、1982年生まれと、Erlangの生まれた1986年と近いですね