Elixir試飲録 (2) – カルチャーショックに戸惑う: 並行指向プログラミング

昨夜、Dave Thomas氏の「Programming Elixir」を読み終えて、こうしてブログを書いている。

読み終えて「とんでもないところに足を踏み入れたな」と思う。漂うのは、何もかもが今までのプログラミングモデルとは違うという圧倒的なアウェイ感である。

Dave Thomas氏は度々このブログにも登場しているが、『達人プログラマー』の著者であり、アジャイルソフトウェア開発宣言の発起人の一人でもある。近年はRubyの人という感じであったが、ElixirにRuby以来の衝撃を受けて、この「Programming Elixir」を執筆するに至ったとのこと。

この本が素晴らしいのは、元々はオブジェクト指向のプログラマーであった著者が関数型言語であるElixirに出会って、徐々にそのコンセプトに体を慣らせて行く過程を、読者が追体験できるところである。この本で扱っているのは Elixir やその基礎となっている Erlang/OTP のほんの導入部分のみであり、主眼はオブジェクト指向プログラマーが関数型あるいは並行指向プログラミングへとパラダイムシフトするための手助けをするといった趣向になっている。

「Programming Elixir」のサンプルプログラムを動かしながら一通りのトピックを見て回り、Elixir(というかErlang)で最も重要なのは、関数型プログラミングではなく、「並行指向プログラミング」と呼ばれる、そのプロセス管理の仕組みであることが分かった。関数型言語はあらゆるプラットフォームで利用出来るが、この並行処理のモデルはおそらくErlangにしかないものなのだろう。

Elixirの並行処理の威力を見るにはサンプルコードを実行するのが手っ取り早い。

まず、Elixirのサイトにある一番始めのサンプルコード(Hello world)が、いきなり並行処理の例になっている。

parent = self()

spawn_link(fn ->
  send parent, {:msg, "hello world"}
end)

receive do
  {:msg, contents} -> IO.puts contents
end

これを hello.exs というファイルに保存して、

$ elixir hello.exs 

のように実行すると、

hello world

となる。

Elixirを知らない人でも、このコードが何をしているかは、何となく分かるのではないだろうか。

  1. spawn_linkという関数に渡された関数が、このプログラムを実行しているメインのプロセスとは別のプロセスとして起動されて、関数の内容を実行する。
  2. 新しく作られたプロセス側では、メインプロセス側(parent)に “hello world” というメッセージを送る。
  3. メインプロセス側は、どこからかメッセージが来ないかを待ち受けて(receive)、メッセージが来たらそれをコンソールに表示する。

このように、spawn でプロセスを作り、sendreceive でメッセージのやり取りをするという単純な仕組みである。プロセスの間でのやり取りはこのメッセージ交換だけで、可変データを共有するということがないので、従来の並行処理について回る排他処理などが必要にならないというのが Elixirの強みである。

この例では一つのプロセスを作っただけなので、他の言語でも同様のことは簡単に出来るかもしれない。では、以下の例だとどうだろうか。

defmodule Chain do
  def counter(next_pid) do
    receive do
      n -> send next_pid, n + 1
    end
  end

  def create_processes(n) do
    last = Enum.reduce 1..n, self,
      fn (_, send_to) -> spawn(Chain, :counter, [send_to]) end
    send last, 0
    receive do
      final_answer when is_integer(final_answer) ->
        "Result is #{inspect(final_answer)}"
    end
  end

  def run(n) do
    IO.puts inspect :timer.tc(Chain, :create_processes, [n])
  end
end

いきなり複雑な例になったが、やっていることは単純である。run関数が呼ばれると、指定された数(n)だけのプロセスを次から次に立ち上げて、それらのプロセスには順番に次のプロセスへの参照を持たせて数珠つなぎのような構造にする(終端はメインプロセス)。全てのプロセスを立ち上げたら、先頭に数値「0」を送ると、プロセスの間でバケツリレーをしながら、その数値に「1」を加えて行く。最後はメインプロセスに到達して結果の値をコンソールに書き出す。この一連の処理は、Erlangの関数である :timer.tc によって時間が計測されており、結果とともに経過時間も出力される。

さて、このコードを chain.exs というファイルに保存して、以下のように実行したらどうなるだろうか?

$ elixir --erl "+P 1000000" -r chain.exs  -e "Chain.run(1_000_000)"

生成するプロセスの数はなんと100万個である。デフォルトのセッティングでは同時並行プロセス数の上限に達してしまうので、オプション(--erl "+P 1000000")で上限を変更している。

これを筆者のPC(Macbook Pro – 3 GHz Intel Core i7, 16GB RAM)で実行した結果が以下である:

{7011658, "Result is 1000000"}

一つ目の数字の単位はマイクロ秒なので、実に100万個のプロセスを起動して連携させる処理がたったの7秒程度で終わったということになる。

何故このようなことが可能なのかと言えば、これらのプロセスがOSで管理されているプロセスやスレッドなどではなく、Erlang VMの中で管理されている独自のプロセスだからである。このErlangが提供する極めて軽量なプロセスモデルが、Elixir/Erlang環境が単なるプログラミング言語以上のインパクトを持つ所以となっている。

このようにElixirでは、一つのプロセスが極めて軽量なため、一つのプロセスを作るのは、オブジェクト指向システムで一つのオブジェクトを作るのと同じぐらいの感覚であると「Programming Elixir」では説明されている。実際プロセスの中には状態を持つものもあり、それらがメッセージでやり取りをするとなれば、モデル的にはオブジェクトと変わりないように思える。Elixirでは、状態を持つプロセスを「Agent(エージェント)」として容易に管理する仕組みがあり、これは、かつてポスト・オブジェクト指向の有力候補と呼ばれていたエージェント指向ではないかと、はたと思い当たったが、それは多分違うような気がする。

Elixir試飲録 (3) – マルチコア危機によるパラダイムシフト: オブジェクト指向から並行指向へ

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中