Skip to content

Instantly share code, notes, and snippets.

@Gab-km
Last active August 29, 2015 14:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gab-km/bb57bdd3c33848287d88 to your computer and use it in GitHub Desktop.
Save Gab-km/bb57bdd3c33848287d88 to your computer and use it in GitHub Desktop.
公式チュートリアルである "Getting Started with Erlang User's Guide" の翻訳 - Concurrent Programming (翻訳中)

<前のページ <https://gist.github.com/Gab-km/ed96c640970f367b911a 次のページ>>

3 並列プログラミング

3.1 プロセス

他の関数型言語の代わりに Erlang を使う主な理由の1つは、並列性と分散プログラミングを扱う Erlang の能力です。並列性によって、同時に実行するいくつかのスレッドを扱うことが出来るプログラムが重要になります。例えば、モダンな OS のおかげでワープロや表計算、メールクライアントに印刷ジョブ、それら全てが同時に実行できます。勿論システムの各プロセッサ(CPU)は一度に1スレッド(またはジョブ)しか扱っていないかもしれませんが、それら全てを同時に実行するイリュージョンをやってのけているような速度で、ジョブを相互に切り替えています。Erlang のプログラムを実行する並行スレッドを作ることは簡単ですし、これらのスレッド間でやりとりさせることも簡単です。Erlang では、各実行スレッドを プロセス と呼びます。

(余談: 「プロセス」という用語は普段、実行スレッドが互いにデータを共有しない時に使われ、「スレッド」という用語は何らかの方法でデータを共有する時に使われます。Erlang における実行スレッドはデータを共有しないので、プロセスと呼びます)

Erlang の BIF である 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 関数は第2引数で指定された回数だけ第1引数を書くことが分かります。今度は start 関数を見てみましょう。この関数は2つの Erlang プロセス、一方は "hello" を3回書くものともう一方は "goodbye" を3回書くもの、これらを開始します。これら両方のプロセスは say_something 関数を使います。プロセスを開始するために spawn によってこのように使われる関数はモジュールからエクスポート(つまり、モジュールの先頭にある -export で)されていなければなりません。

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

"hello" を3回、それから "goodbye" を3回、とは書いておらず、1番目のプロセスが "hello" を書いて、2番目が "goodbye" を、1番目が別の "hello" を書いて、ほげほげ。しかし、<0.63.0> はどこからやってきたのでしょうか?関数の戻り値は、もちろん関数の最後に「やったこと」の戻り値です。 start 関数が最後にやったことは

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

spawnプロセス識別子 、つまり pid を返しますが、これはプロセスを一意に識別するものです。なので <0.63.0> は上で呼んだ spawn 関数の pid なのです。次の例でどのように pid を使うか見てみましょう。

それと、 io:format の中で ~w の代わりに ~p を使いましたね。マニュアルにはこうあります: 「~p は標準の構文で ~w と同じようにデータを書きますが、印字すると1行より長くなるタームを複数行に分割し、各行をいい感じにインデントします。また、印字できる文字のリストを検知して、これらを文字列として出力しようと試みます」。

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 関数はまずプロセスを作りますが、これを "pong" と呼びましょう:

Pong_PID = spawn(tut15, pong, [])

このプロセスは tut15:pong() を実行します。 Pong_PID は "pong" プロセスのプロセス識別です。 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 タームです。つまり、リストでもいいし、タプルや整数、アトム、pid などでも構いません。

各プロセスは受信したメッセージ用の専用の入力キューを持ちます。受信した新しいメッセージはキューの末尾に投入されます。プロセスが receive を処理する時、キューの最初のメッセージが receive 内の最初のパターンにマッチされ、これがマッチするとそのメッセージはキューから取り除かれ、パターンに対応したアクションが実行されます。

しかし、最初のパターンがマッチしない場合、2番目のパターンがテストされ、マッチしたらこのメッセージがキューから取り除かれて、2番目のパターンに対応するアクションが実行されます。2番目のパターンがマッチしない場合、3番目が試されて、テストできるパターンがなくなるまで続きます。テストするパターンがなくなった場合、最初のメッセージはキューに保持され、代わりに2番目のメッセージにチャレンジします。これが何らかのパターンにマッチすると、適切なアクションが実行され、2番目のメッセージがキューから取り除かれます(キューの最初のメッセージとそれ以外のメッセージは保持されたままです)。2番目のメッセージがマッチしない場合、3番目のメッセージに挑戦し、キューの終端に到達するまで続きます。キューの終端に到達した場合、プロセスはブロック(実行を停止)し、新しいメッセージを受信するまで待ち、この処理が繰り返されます。

もちろん、Erlang の実装は「賢く」て、各 receive にあるパターンに対して各メッセージがテストされる回数を最小化します。

ではピンポンの例に戻りましょう。

"Pong" はメッセージを待ち受けています。アトム finished を受信した場合、"pong" は "Pong finished" を出力に書き込み、他にすることがなくなったので、終了します。もし次のようなフォーマットのメッセージ:

{ping, Ping_PID}

を受信すると、"Pong received ping" を出力に書き込み、"ping" プロセスに向けて pong アトムを送信します:

Ping_PID ! pong

"!" 演算子がメッセージを送信するのにどのように使われるか説明します。"!" の構文は次のとおりです:

Pid ! Message

つまり、 Message (任意の Erlang ターム) は識別 Pid を持つプロセスに対して送信されます。

"ping" プロセスに対し、 pong メッセージを送信した後、"pong" は pong 関数を再度呼び出しますが、これは "pong" を receive に戻らせ、別のメッセージを待ち受けさせます。

tut15:ping(3, Pong_PID)

ping/2 関数を見てみると、第1引数の値が3(0じゃない)なので ping/2 の2番目の節が実行されるのが分かります(最初の節のヘッドは ping(0,Pong_PID) で、2番目の節のヘッドは ping(N,Pong_PID) なので、 N は3になります)。

2番目の節は "pong" に以下のメッセージを送信します:

Pong_PID ! {ping, self()},

self()self() を実行したプロセスの pid を返し、今回のケースでは "ping" の pid です。("pong" のコードを思い出してください、これは先に説明した receive 内の変数 Ping_PID になるでしょう)。

そして "Ping" は "pong" からの応答を待ち受け:

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

応答が届くと "Ping received pong" を書きますが、その後 "ping" が ping 関数を再び呼び出します。

ping(N - 1, Pong_PID)

N-1 は第1引数が0になるまでデクリメントされていきます。こうなると、 ping/2 の最初の節が実行されます:

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

finished アトムが "pong" に送信され(上で述べたように、処理が終了します)、出力に "ping finished" が書き込まれます。何もやり残したことがなくなったので、 "Ping" は自身を終了させます。

3.3 登録済みプロセス名

上の例で、"ping" を開始した時に "pong" の識別を与えられるようにするために、まず "pong" を作りました。つまり、"pong" にメッセージを送るために、どうにかして "ping" は "pong" の識別を知らなければならないということです。互いの識別を知る必要があるプロセスは、時として互いに完全に独立して開始されます。Erlang はそれゆえ、pid の代わりに識別として使うことが出来る名前をプロセスが得るためのメカニズムを提供しています。これは register BIF を使うことで達成できます:

register(some_atom, Pid)

これと "pong" プロセスに与える 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 ! {ping, self()},

これで引数 Pong_PID を使う必要がなくなったので、 ping/2ping/1 になります。

3.4 分散プログラミング

さぁ、別のコンピューター上にある "ping" と "pong" を使ったピンポンのプログラムをに書き換えてみましょう。これをする前に、これを作動させるための準備が少しばかり必要です。分散 Erlang 実装は、別のコンピューター上にある Erlang システムに対する権限のないアクセスを防止するための基本的なセキュリティ機構を提供します (*マニュアル*)。お互いにやりとりをする Erlang システムは同じ マジッククッキー を持たねばなりません。これを達成するための最も簡単な方法は、お互いにコミュニケーションを取る Erlang システムを実行しようという全てのマシンのホームディレクトリに .erlang.cookie と呼ばれるファイルを持つことです(Windows システムにおいて、ホームディレクトリは $HOME 環境変数 - これを設定する必要があるかもしれません - によって指定されたディレクトリです。Linux や Unix においては、この環境変数を安全に無視することができて、単に .erlang.cookie というファイルを、引数なしで cd コマンドを実行したあとに得られるディレクトリに作成します)。 .erlang.cookie ファイルは同じアトムの1行を含まないといけません。Linux や Unix 上の例として、OS のシェルにおいて:

$ cd
$ cat > .erlang.cookie
this_is_very_secret
$ chmod 400 .erlang.cookie

上の chmod.erlang.cookie ファイルをファイル所有者だけアクセス可能にします。これは必須です。

他の Erlang システムとやりとりしようとしている Erlang システムを開始する時、そのシステムに名前を与えなければいけません、例えば次のようにします:

$ erl -sname my_name

詳細は後ほど見てみましょう(*マニュアル*)。分散 Erlang を使って実験したい、でも動かせるコンピューターが1つしかないという場合、同じコンピュータ上ですが別の名前をつけた2つの Erlang システムをスタートさせることができます。コンピューター上で実行中の各 Erlang システムは Erlang ノードと呼ばれます。

(注記: erl -sname は全てのノードは同じ IP ドメイン内にいることを想定しており、IP アドレスの最初のコンポーネントのみ使うことができ、別のドメインにあるノードを使いたい場合は代わりに -name を使いますが、全ての IP アドレスを省略なしで与えなければいけません(*マニュアル*))。

2つの別々のノード上で実行するよう修正したピンポンの例がこちらになります:

-module(tut17).

-export([start_ping/1, start_pong/0,  ping/2, pong/0]).

ping(0, Pong_Node) ->
    {pong, Pong_Node} ! finished,
    io:format("ping finished~n", []);

ping(N, Pong_Node) ->
    {pong, Pong_Node} ! {ping, self()},
    receive
        pong ->
            io:format("Ping received pong~n", [])
    end,
    ping(N - 1, Pong_Node).

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

start_pong() ->
    register(pong, spawn(tut17, pong, [])).

start_ping(Pong_Node) ->
    spawn(tut17, ping, [3, Pong_Node]).

gollum と kosken という2つのコンピューターがあるとしましょう。ping という kosken のノードを開始し、それから pong という gollum のノードを開始します。

kosken にて(Linux/Unix システム上で):

kosken> erl -sname ping
Erlang (BEAM) emulator version 5.2.3.7 [hipe] [threads:0]

Eshell V5.2.3.7  (abort with ^G)
(ping@kosken)1>

gollum にて:

gollum> erl -sname pong
Erlang (BEAM) emulator version 5.2.3.7 [hipe] [threads:0]

Eshell V5.2.3.7  (abort with ^G)
(pong@gollum)1>

さぁ、gollum 上の "pong" プロセスを開始し:

(pong@gollum)1> tut17:start_pong().
true

そして、kosken 上の "ping" プロセスを開始しましょう(上記コードから、 start_ping 関数のパラメータは "pong" を実行している Erlang システムのノード名でだと分かるでしょう):

(ping@kosken)1> tut17:start_ping(pong@gollum).
<0.37.0>
Ping received pong
Ping received pong 
Ping received pong
ping finished

はい、ピンポンプログラムが実行し、"pong" 側では次のようになっています:

(pong@gollum)2>
Pong received ping                 
Pong received ping                 
Pong received ping                 
Pong finished                      
(pong@gollum)2>

tut17 のコードを見て、 pong 関数自身は変わっていません、次のコード:

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

は "ping" プロセスを実行しているノードに関わらず同様に動作します。ですから Erlang pid はどこでプロセスが実行しているかについての情報を持っているので、プロセスの pid を知っていれば、"!" 演算子はその識別にそのプロセスが同じノード上か別のノード上かのメッセージを送るのに使うことが出来ます。

違いは別のノード上の登録済みプロセスにどうやってメッセージを送るか、です:

{pong, Pong_Node} ! {ping, self()},

そのまま registered_name を使う代わりに、タプル {registerd_name,node_name} を使います。

前の例で、"ping" と "pong" を2つの別の Erlang ノード上のシェルから開始しました。 spawn も別のノードのプロセスを開始するのに使うことが出来ます。次の例は、まだピンポンプログラムですが、今度は別のノードにある "ping" を開始します:

-module(tut18).

-export([start/1,  ping/2, pong/0]).

ping(0, Pong_Node) ->
    {pong, Pong_Node} ! finished,
    io:format("ping finished~n", []);

ping(N, Pong_Node) ->
    {pong, Pong_Node} ! {ping, self()},
    receive
        pong ->
            io:format("Ping received pong~n", [])
    end,
    ping(N - 1, Pong_Node).

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

start(Ping_Node) ->
    register(pong, spawn(tut18, pong, [])),
    spawn(Ping_Node, tut18, ping, [3, node()]).

ping という("ping" プロセスではない) Erlang システムが既に kosken 上で開始されていると想定し、gollum 上で次を実行します:

(pong@gollum)1> tut18:start(ping@kosken).
<3934.39.0>
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong finished
ping finished

gollum 上ですべての出力が得られたことに触れておきましょう。これは io システムがどこでプロセスが生成されたか分かり、そこにすべての出力を送ったからなのです。

3.5 もっと大きな例

さて、もっと大きな例ですよ。極端にシンプルな「メッセンジャー」を作ります。メッセンジャーはユーザーが異なるノードにログインできるようにし、お互いにシンプルなメッセージを送り合えるようにするプログラムです。

始める前に、以下をノートにとっておきましょう:

  • この例は何の試みもないメッセージ・パッシングのロジックがイカした GUI を提供するようになっているということを示すでしょう - これは勿論 Erlang がやってくれることです - が、それは別のチュートリアルです。
  • この手の問題は OTP にある機能を使えばもっと簡単に解くことができますが、OTP は実行中のコードを更新する方法なども提供します。ですが、それは別のチュートリアルです(2回目)。
  • 最初に書いたプログラムには、消えてしまったノードの扱いについてはいくつか不適切なところが含まれますが、これらはプログラムの後のバージョンで直していきます。

「クライアント」が中央サーバーに接続できるようにし、どこに誰がいるか言えるようにすることでメッセンジャーを作り上げます。つまり、ユーザーはメッセージを送るために、別のユーザーがいる Erlang ノードの名前を知る必要がないということです。

messenger.erl ファイルです:

%%% メッセージ・パッシング ユーティリティ
%%% ユーザーインターフェイス:
%%% logon(Name)
%%%     メッセンジャーシステムでは Erlang ノードごとに一度に1ユーザー
%%%     がログインできます: そしてお気に入りの名前を選びます。もし
%%%     その名前が別のノードで既にログイン済みだったり、他の誰かが
%%%     同じノードにログイン済みの場合、適切なエラーメッセージを
%%%     伴ってログインは拒否されます。
%%% logoff()
%%%     そのノードにいる任意の人をログオフさせます
%%% message(ToName, Message)
%%%     ToName に対して Message を送信します。その関数のユーザーが
%%%     ログオンしていないか、ToName がどのノードにもログオンしていない
%%%     場合、エラーメッセージが表示されます。
%%%
%%% Erlang ノードのネットワークにあるノードがログオン中のユーザについての
%%% データを保持するサーバーを実行します。サーバは "messenger" として登録
%%% されます。ログオン中のユーザーがいるノードごとに "mess_client" として
%%% 登録されたクライアントプロセスを実行します。
%%%
%%% クライアントプロセスとサーバ間のプロトコル
%%% ------------------------------------------
%%% 
%%% サーバー宛: {ClientPid, logon, UserName}
%%% 返信 {messenger, stop, user_exists_at_other_node} クライアントを停止させます
%%% 返信 {messenger, logged_on} ログオンは成功しました
%%%
%%% サーバー宛: {ClientPid, logoff}
%%% 返信: {messenger, logged_off}
%%%
%%% サーバー宛: {ClientPid, lofoff}
%%% 返信: リプライはありません
%%%
%%% サーバー宛: {ClientPid, messege_to, ToName, Message} メッセージを送信します
%%% 返信: {messenger, stop, you_are_not_logged_on} クライアントを停止させます
%%% 返信: {messenger, receiver_not_found} この名前のユーザーはログオンしていません
%%% 返信: {messenger, sent} メッセージは送信されました(送達保証はありません)
%%%
%%% クライアント宛: {message_from, Name, Message},
%%%
%%% 「コマンド」とクライアント間のプロトコル
%%%
%%% 開始後: messenger:client(Server_Node, Name)
%%% クライアント宛: logoff
%%% クライアント宛: {message_to, ToName, Message}
%%%
%%% 設定: メッセンジャーサーバーが実行しているノード名を返すために
%%% server_node() 関数を変更しましょう

-module(messenger).
-export([start_server/0, server/1, logon/1, logoff/0, message/2, client/2]).

%%% メッセンジャーサーバーが動作しているノードの名前を返すために以下の
%%% 関数を変更します
server_node() ->
    messenger@bill.

%%% これは "messenger" 用のサーバープロセスです
%%% ユーザーのリストは [{ClientPid1, Name1},{ClientPid2, Name2},...] という形式です
server(User_List) ->
    receive
        {From, logon, Name} ->
            New_User_List = server_logon(From, Name, User_List),
            server(New_User_List);
        {From, logoff} ->
            New_User_List = server_logoff(From, User_List),
            server(New_User_List);
        {From, message_to, To, Message} ->
            server_transfer(From, To, Message, User_List),
            io:format("list is now: ~p~n", [User_List]),
            server(User_List)
    end.

%%% サーバーを開始します
start_server() ->
    register(messenger, spawn(messenger, server, [[]])).


%%% サーバーがユーザーリストに新しいユーザーを追加します
server_logon(From, Name, User_List) ->
    %% 他のどこかでログオンしていないかチェックします
    case lists:keymember(Name, 2, User_List) of
        true ->
            From ! {messenger, stop, user_exists_at_other_node},  %reject logon
            User_List;
        false ->
            From ! {messenger, logged_on},
            [{From, Name} | User_List]        %add user to the list
    end.

%%% サーバーがユーザーリストからユーザーを削除します
server_logoff(From, User_List) ->
    lists:keydelete(From, 1, User_List).


%%% サーバーがユーザー間のメッセージを変換します
server_transfer(From, To, Message, User_List) ->
    %% ユーザーがログオン中か、および誰なのかをチェックします
    case lists:keysearch(From, 1, User_List) of
        false ->
            From ! {messenger, stop, you_are_not_logged_on};
        {value, {From, Name}} ->
            server_transfer(From, Name, To, Message, User_List)
    end.
%%% ユーザーが存在する場合、メッセージを送信します
server_transfer(From, Name, To, Message, User_List) ->
    %% レシーバーを探し出し、メッセージを送信します
    case lists:keysearch(To, 2, User_List) of
        false ->
            From ! {messenger, receiver_not_found};
        {value, {ToPid, To}} ->
            ToPid ! {message_from, Name, Message}, 
            From ! {messenger, sent} 
    end.


%%% ユーザーコマンドです
logon(Name) ->
    case whereis(mess_client) of 
        undefined ->
            register(mess_client, 
                     spawn(messenger, client, [server_node(), Name]));
        _ -> already_logged_on
    end.

logoff() ->
    mess_client ! logoff.

message(ToName, Message) ->
    case whereis(mess_client) of % Test if the client is running
        undefined ->
            not_logged_on;
        _ -> mess_client ! {message_to, ToName, Message},
             ok
end.


%%% サーバーノードごとに起動しているクライアントプロセスです
client(Server_Node, Name) ->
    {messenger, Server_Node} ! {self(), logon, Name},
    await_result(),
    client(Server_Node).

client(Server_Node) ->
    receive
        logoff ->
            {messenger, Server_Node} ! {self(), logoff},
            exit(normal);
        {message_to, ToName, Message} ->
            {messenger, Server_Node} ! {self(), message_to, ToName, Message},
            await_result();
        {message_from, FromName, Message} ->
            io:format("Message from ~p: ~p~n", [FromName, Message])
    end,
    client(Server_Node).

%%% サーバーからの返信を待ちます
await_result() ->
    receive
        {messenger, stop, Why} -> % Stop the client 
            io:format("~p~n", [Why]),
            exit(normal);
        {messenger, What} ->  % Normal response
            io:format("~p~n", [What])
    end.

To use this program you need to:

configure the server_node() function copy the compiled code (messenger.beam) to the directory on each computer where you start Erlang. In the following example of use of this program, I have started nodes on four different computers, but if you don't have that many machines available on your network, you could start up several nodes on the same machine.

We start up four Erlang nodes, messenger@super, c1@bilbo, c2@kosken, c3@gollum.

First we start up a the server at messenger@super:

(messenger@super)1> messenger:start_server(). true Now Peter logs on at c1@bilbo:

(c1@bilbo)1> messenger:logon(peter). true logged_on James logs on at c2@kosken:

(c2@kosken)1> messenger:logon(james). true logged_on and Fred logs on at c3@gollum:

(c3@gollum)1> messenger:logon(fred). true logged_on Now Peter sends Fred a message:

(c1@bilbo)2> messenger:message(fred, "hello"). ok sent And Fred receives the message and sends a message to Peter and logs off:

Message from peter: "hello" (c3@gollum)2> messenger:message(peter, "go away, I'm busy"). ok sent (c3@gollum)3> messenger:logoff(). logoff James now tries to send a message to Fred:

(c2@kosken)2> messenger:message(fred, "peter doesn't like you"). ok receiver_not_found But this fails as Fred has already logged off.

First let's look at some of the new concepts we have introduced.

There are two versions of the server_transfer function, one with four arguments (server_transfer/4) and one with five (server_transfer/5). These are regarded by Erlang as two separate functions.

Note how we write the server function so that it calls itself, server(User_List) and thus creates a loop. The Erlang compiler is "clever" and optimizes the code so that this really is a sort of loop and not a proper function call. But this only works if there is no code after the call, otherwise the compiler will expect the call to return and make a proper function call. This would result in the process getting bigger and bigger for every loop.

We use functions in the lists module. This is a very useful module and a study of the manual page is recommended (erl -man lists). lists:keymember(Key,Position,Lists) looks through a list of tuples and looks at Position in each tuple to see if it is the same as Key. The first element is position 1. If it finds a tuple where the element at Position is the same as Key, it returns true, otherwise false.

3> lists:keymember(a, 2, [{x,y,z},{b,b,b},{b,a,c},{q,r,s}]). true 4> lists:keymember(p, 2, [{x,y,z},{b,b,b},{b,a,c},{q,r,s}]). false lists:keydelete works in the same way but deletes the first tuple found (if any) and returns the remaining list:

5> lists:keydelete(a, 2, [{x,y,z},{b,b,b},{b,a,c},{q,r,s}]). [{x,y,z},{b,b,b},{q,r,s}] lists:keysearch is like lists:keymember, but it returns {value,Tuple_Found} or the atom false.

There are a lot more very useful functions in the lists module.

An Erlang process will (conceptually) run until it does a receive and there is no message which it wants to receive in the message queue. I say "conceptually" because the Erlang system shares the CPU time between the active processes in the system.

A process terminates when there is nothing more for it to do, i.e. the last function it calls simply returns and doesn't call another function. Another way for a process to terminate is for it to call exit/1. The argument to exit/1 has a special meaning which we will look at later. In this example we will do exit(normal) which has the same effect as a process running out of functions to call.

The BIF whereis(RegisteredName) checks if a registered process of name RegisteredName exists and return the pid of the process if it does exist or the atom undefined if it does not.

You should by now be able to understand most of the code above so I'll just go through one case: a message is sent from one user to another.

The first user "sends" the message in the example above by:

messenger:message(fred, "hello") After testing that the client process exists:

whereis(mess_client) and a message is sent to mess_client:

mess_client ! {message_to, fred, "hello"} The client sends the message to the server by:

{messenger, messenger@super} ! {self(), message_to, fred, "hello"}, and waits for a reply from the server.

The server receives this message and calls:

server_transfer(From, fred, "hello", User_List), which checks that the pid From is in the User_List:

lists:keysearch(From, 1, User_List) If keysearch returns the atom false, some sort of error has occurred and the server sends back the message:

From ! {messenger, stop, you_are_not_logged_on} which is received by the client which in turn does exit(normal) and terminates. If keysearch returns {value,{From,Name}} we know that the user is logged on and is his name (peter) is in variable Name. We now call:

server_transfer(From, peter, fred, "hello", User_List) Note that as this is server_transfer/5 it is not the same as the previous function server_transfer/4. We do another keysearch on User_List to find the pid of the client corresponding to fred:

lists:keysearch(fred, 2, User_List) This time we use argument 2 which is the second element in the tuple. If this returns the atom false we know that fred is not logged on and we send the message:

From ! {messenger, receiver_not_found}; which is received by the client, if keysearch returns:

{value, {ToPid, fred}} we send the message:

ToPid ! {message_from, peter, "hello"}, to fred's client and the message:

From ! {messenger, sent} to peter's client.

Fred's client receives the message and prints it:

{message_from, peter, "hello"} ->

io:format("Message from ~p: ~p~n", [peter, "hello"])

and peter's client receives the message in the await_result function.

<前のページ <https://gist.github.com/Gab-km/ed96c640970f367b911a 次のページ>>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment