M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

Erlangチュートリアルを読む (3)

並行プログラムの章の前半です。原典は、“Getting Started with Erlang User's Guide - Version 5.8.4 - May 24 2011”(2011/07/09取得)です。

******************************

3 並行プログラミング

3.1 プロセス

〔訳者:以下の説明の中で「実行スレッド」と「スレッド」というのは概念として別であることに注意してください。「スレッド」は「プロセス」と対比される概念です。そしてこの対比される2概念は「実行スレッド」のあり方・実装に対応するものです。いずれにしても「実行スレッド」とは、抽象的な概念で、文字通り処理がはじまり、決められた項目が順々に処理されていき、いずれ終了する、あの一続きの「糸」のことです。〕

他の関数型言語でなくErlangを使用するときの主要な理由のひとつは、Erlangの並行性〔concurrency〕と分散プログラミングの能力にあります。並行性により、プログラムは同時にいくつもの実行スレッドを処理することができます。例えばモダンなOSはあなたにワードプロセッサーやスプレッドシート、メールクライアントやプリント作業などを同時に実行させてくれます。もちろんシステム内の個々のプロセッサー(CPU)が同一時間に処理できるスレッド(もしくはジョブ)はおそらく1つだけです。しかしジョブ〔もしくはスレッド〕の間で互いに交代を繰り返すことで、まるですべてが同一時間に実行されているようなイリュージョンを見せてくれるのです。Erlangプログラムで並列〔parallel〕実行スレッドを生成し、それらのスレッド同士で通信させるのは簡単です。Erlangでは実行スレッドのことをプロセス〔process〕と呼んでいます。

(余談:“プロセス”〔process〕という言葉は、互いにデータを共有しない実行スレッドを指してよく使用されます。一方、“スレッド”〔thread〕という言葉は、とくにそれらの間で何らかの方法でデータを共有する実行スレッドを指して使用されます。Erlangにおける実行スレッドは互いにデータを共有しません。私たちがそれをプロセスと呼ぶのはこのためです。)

Erlang組み込み関数であるspawnは、次の書式で新しいプロセスを生成するために使用します:spawn(Module, Exported_Function, List_of_Arguments)

-module(tut14).
-export([start/0, say_something/2]).
say_something(What, 0) ->
    done;
say_something(What, Times) ->
    io:format("~p~n", [What]),
    say_something(What, Times - 1).
start() ->
    spawn(tut14, say_something, [hello, 3]),
    spawn(tut14, say_something, [goodbye, 3]).
5> c(tut14).
{ok,tut14}
6> tut14:say_something(hello, 3).
hello
hello
hello
done

ご覧のようにsay_something関数は、第1引数で指定したものを、第2引数で指定した回数だけ出力します。それではstart関数を見ていきましょう。この関数は2つのErlangプロセスを開始させます。プロセスの1つは“hello”と3回出力し、もうひとつは“goodbye”と3回出力します。どちらのプロセスもsay_something関数を使用しています。このようにspawn関数でプロセスとして開始させることのできる関数は、モジュールが外部に公開しているもの(すなわちモジュールの先頭部の-exportによって公開されているもの)に限られる点に注意してください。
〔訳者:つまり上の例ではstart関数は単に同一モジュール内の関数をspawnによってプロセス化しているわけですが、それでもその対象の関数はモジュール外部に公開されている必要がある、という点が強調されているのです。〕

9> tut14:start().
hello
goodbye
<0.63.0>
hello
goodbye
hello
goodbye

“hello”が3回そして“goodbye”が3回という出力にはなっていません〔訳者:単に逐次的に実行した──say_somethingを順番に呼び出した──だけなら、そうなるはずです〕。最初のプロセスは“hello”と出力し、第2のプロセスが“goodbye”と出力し、また第1のプロセスが“hello”と出力し…。それにしても<0.63.0>というのはどこから来たのでしょうか? もちろん関数の返り値は関数の最後の“もの”の値です。start関数の場合この最後のものは、

spawn(tut14, say_something, [goodbye, 3]).

spawn関数はプロセスID──もしくはpidを返します。pidはプロセスを一意に識別するものです。<0.63.0>は〔2番目の〕spawn関数が返したpidだったのです。われわれは次の例でこのpidをどのように使用するか見ていきます。

もう一点。先ほどの例ではio:format関数で~wではなく~pを使用しました。マニュアルから引用します:「~p~wと同じ方法でデータを標準的構文によって表示します。しかし表示する表現〔terms〕が一行に収まりきらない場合に複数行で表示するために改行を行い、しかも各行にかしこくインデントを施します。~pはまた出力可能な文字のリストを検出し、文字列として出力することを試みます」。

3.2 メッセージ渡し

次の例では任意の回数分互いにメッセージを送りあう2つのプロセスを生成しています。

-module(tut15).
-export([start/0, ping/2, pong/0]).
ping(0, Pong_PID) ->
    Pong_PID ! finished,
    io:format("ping finished~n", []);
ping(N, Pong_PID) ->
    Pong_PID ! {ping, self()},
    receive
        pong ->
            io:format("Ping received pong~n", [])
    end,
    ping(N - 1, Pong_PID).
pong() ->
    receive
        finished ->
            io:format("Pong finished~n", []);
        {ping, Ping_PID} ->
            io:format("Pong received ping~n", []),
            Ping_PID ! pong,
            pong()
    end.
start() ->
    Pong_PID = spawn(tut15, pong, []),
    spawn(tut15, ping, [3, Pong_PID]).
1> c(tut15).
{ok,tut15}
2> tut15: start().
<0.36.0>
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
ping finished
Pong finished

start関数ははじめに1つプロセスを生成します。これを“pong”と呼びましょう:

Pong_PID = spawn(tut15, pong, [])

このプロセスはtus15:pong()を実行します。Pong_PIDは“pong”プロセスのpidです。start関数はつづいて別のプロセス“ping”を生成します。

spawn(tut15, ping, [3, Pong_PID]),

この関数は、tut15:ping(3, Pong_PID)を実行します。

<0.36.0> start関数の返り値です。

さて、“pong”プロセスが実行されます:

receive
    finished ->
        io:format("Pong finished~n", []);
    {ping, Ping_PID} ->
        io:format("Pong received ping~n", []),
        Ping_PID ! pong,
        pong()
end.

receive構造は、そのプロセスをして他のプロセスから送信されてくるメッセージを待つことを可能にします。形式は以下の通り:

receive
   pattern1 ->
       actions1;
   pattern2 ->
       actions2;
   ....
   patternN
       actionsN
end.

endの前には“;”がないことに注意してください。

Erlangプロセス間で交換されるメッセージは、単なる妥当なErlang表現〔terms〕です。つまりリスト、タプル、整数、アトム、pidその他諸々です。

各プロセスはメッセージを受信するために自分用の入力待ち行列〔input queue〕を持っています。新しく受信されたメッセージはこの行列の末端に追加されます。あるプロセスが自身のreceive構造を実行するとき、待ち行列の中の先頭のメッセージがreceive構造の最初のパターンに照合されます。もしマッチすればメッセージは待ち行列から削除され、そのパターンに対応するactionが実行されます。

しかし最初のパターンにマッチしなかった場合、第2のパターンがチェックされます。もしマッチすればメッセージは待ち行列から削除され、第2のパターンに対応するactionが実行されます。第2のパターンが適合しなければ、第3のそれがチェックされます。そうしてパターンがなくなるまでチェックが繰り返されます。もしいずれのパターンも適合しなかったら、最初のメッセージはそのまま待ち行列に残し、2番目のメッセージがパターンマッチ作業の対象になります。もしこのメッセージがいずれかのパターンに適合したら、適切なactionが実行されて、メッセージは待ち行列から削除されます(1番目のメッセージとその他のすべてのメッセージは依然として待ち行列に保持されています)。もし2番目のメッセージが適合しなければ、3番目のメッセージがパターンマッチ作業の対象になり、これが待ち行列の終端に至るまで繰り返されます。待ち行列の終わりに来ると、プロセスはブロックし(実行を停止し)新しいメッセージが届くのを待ちます。この処理が繰り返されます。

もちろんErlang実装は“賢い”ので、各メッセージがreceive構造内の各パターンと照合される回数を最小限に抑えます。

さてping pong〔ピンポン〕の例に戻りましょう。

“Pong”プロセスはメッセージを待っています。アトムfinishedを受信すると、“pong”プロセスは“Pong finished”と画面に出力する以外何もせず、終了します。一方、次の形式のメッセージを受信すると:

{ping, Ping_PID}

“pong”プロセスは、“Pong received ping”と画面に出力し、“ping”プロセスに対してアトムpongを送信します:

Ping_PID ! pong

“!”演算子がメッセージ送信に使用されています。“!”の構文は次の通り:

Pid ! Message

これによりMessage(何らかのErlang表現)はPidにより識別されるプロセスに送信されます。

ping”プロセス宛にメッセージとしてアトムpongを送信したあと、“pong”プロセスは、再度pong関数を呼びます。これにより〔実行スレッドは〕再度receive構造の前に立ち戻り、メッセージを待つ状態になります。さて、それではプロセス“ping”について見ていきましょう。このプロセスが次の呼び出しにより開始されたことを思い出してください:

tut15:ping(3, Pong_PID)

このping/2関数について見てみましょう。ping/2の第2の節〔clause〕が最初の実行時に受け取る値は3です(0ではありません)(この関数の第1の節はping(0,Pong_PID)で、第2の節がping(N,Pong_PID)です。今回Nは3です)。

この第2の節は“pong”プロセス宛にメッセージを送ります:

Pong_PID ! {ping, self()},

self()はそれを実行したプロセスのpidを返します。この場合、“ping”プロセスのpidが返されます。(“pong”プロセス〔として実行されているpong関数〕のコードを思い出してください。先ほど解説したreceive構造の中で、メッセージとして受け取ったpidは変数Ping_PIDに割り当てられていました)。

ここで“ping”プロセスは“pong”プロセスからの返答を待ちます:

receive
    pong ->
        io:format("Ping received pong~n", [])
end,

返信が届くと、“ping”プロセスは“Ping received pong”と画面に出力し、その後にping関数を再度呼び出します。

ping(N - 1, Pong_PID)

N-1としたことにより第1引数はデクリメントされます。この再帰的呼び出しは0になるまで続きます。このとき、ping/2関数の第1の節が実行されます:

ping(0, Pong_PID) ->
    Pong_PID !  finished,
    io:format("ping finished~n", []);

アトムfinishedが“pong”に送信され(前述のとおりこれにより“pong”プロセスは終了します)、“ping finished”と画面に出力されます。

3.3登録されたプロセス名

上記の例では、“ping”プロセスを開始する際に“pong”プロセスのIDを渡すために、まず“pong”プロセスを生成していました。“ping”プロセスは“pong”プロセスにメッセージを送るために、いずれにしろ何らかの方法でそのIDを知っている必要があったのです。他のプロセスとまったく独立に〔したがって開始順序などに縛られず〕開始されたプロセスのIDを知る必要がある、ということはしばしばあります。このためErlangはプロセスに名前付けを行うメカニズムを提供しています。それらの名前はpidの代わりにプロセスのIDとして使用できます。この機能は組み込み関数registerによって提供されます:

register(some_atom, Pid)

さてこの関数を使用して“pong”プロセスにpongという名前を与えるようにして、ping pongプログラムを書き換えてみましょう:

-module(tut16).
-export([start/0, ping/1, pong/0]).
ping(0) ->
    pong ! finished,
    io:format("ping finished~n", []);
ping(N) ->
    pong ! {ping, self()},
    receive
        pong ->
            io:format("Ping received pong~n", [])
    end,
    ping(N - 1).
pong() ->
    receive
        finished ->
            io:format("Pong finished~n", []);
        {ping, Ping_PID} ->
            io:format("Pong received ping~n", []),
            Ping_PID ! pong,
            pong()
    end.
start() ->
    register(pong, spawn(tut16, pong, [])),
    spawn(tut16, ping, [3]).
2> c(tut16).
{ok, tut16}
3> tut16:start().
<0.38.0>
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
ping finished
Pong finished

start/0関数の中では、

register(pong, spawn(tut16, pong, [])),

“pong”プロセスが生成されpongという名前が与えられます。“ping”プロセスの中では、pong〔という名前のつけられた“pong”プロセス〕宛にメッセージを送信します:

pong ! {ping, self()},

こうしてping/2関数はping/1関数になりました。引数Pong_PIDが不要になったからです。

******************************

原典は、“Getting Started with Erlang User's Guide - Version 5.8.4 - May 24 2011”(2011/07/09取得)です。