パート20: 車輪の中の車輪: Twisted と Erlang

はじめに

この連載を通して、同期の「普通の Python」コードと非同期の Twisted のコードを混在させることは単純なことではない、ということを明らかにしてきました。 Twisted プログラムで不確定な時間のブロッキングがあると、非同期モデルを使って実現しようとしている利点の多くが失われてしまうからです。

今回が初めての非同期プログラミングへの入門ならば、ここまでで身に付けてきた知識はなんだか限定的であるかのように感じられるかもしれません。 Twisted の中では新しいテクニックを使えますが、もっと広い世界である一般的な Python コードでは使えません。 また、Twisted を使っていると、Twisted プログラムの一部として使うために記述されたライブラリを使うように限定されてしまいます。 少なくとも reactor を動かしているスレッドからライブラリのコードを直接呼出したい場合にはそうなります。

しかし非同期プログラミングのテクニックはとてもよく登場するものであり、Twisted に限ったものではありません。 実際、Python に限っても、驚くべきほどたくさんの非同期プログラミングのフレームワークがあります。 ちょっと ググッてみる と、それなりの数がきっと見つかるでしょう。 細かい部分では Twisted とは異なりますが、基本的な考え方 (非同期 I/O, 複数のデータストリームにまたがった小さなチャンクでのデータ処理) は一緒です。 そのため、Twisted とは異なるフレームワークを使う必要がある、もしくは選ぶなら、Twisted で学んできたことを活用できるでしょう。

Python 以外に目を向けてみると、非同期プログラミングモデルを基礎とした、あるいは活用するための言語やシステムがたくさんあります。 Twisted に関する知識は、この分野のさらに広大なことへの探求を手助けし続けてくれるでしょう。

このパートでは、 Erlang について目を通していきます。 Erlang は非同期プログラミングの概念をとてもよく活用したプログラミング言語でありランタイムシステムです。しかし、独特の方法です。 なお、ここでは Erlang に関する一般的な入門を意図していないことに気をつけてくださいね。 むしろ、Erlang に組み込まれた考え方についてのちょっとした探検であり、Twisted における考え方とどのようなつながりがあるかを見ていきます。 他の技術を学ぶときにも、Twisted を学ぶ中で身に付けてきた知識を適用できることが基本的なテーマです。

思い出されるコールバック

図6:reactor がコールバックを扱う様子”を思い出してください。コールバックをグラフィカルに表現したものです。 Poetry Client 3.0 にある本質的なコールバックは、”パート6: さらなる高みへ”で導入され、それ以降のすべての詩のクライアントは dataReceived メソッドにあります。 このコールバックは、接続した詩のサーバのひとつから詩を受け取る度に呼び出されます。

クライアントが異なるみっつのサーバからみっつの詩をダウンロードしていると仮定しましょう。 reactor の視点から見てみると (この連載でもっとも強く協調してきた観点です)、一回りする度にひとつ以上のコールバックを生成するひとつの大きなループを持ちます。 図40を見てください。

_images/p20_reactor-2.png

図40:reactor から見たコールバック

この図は、reactor が詩を受け取ると、 dataReceived を呼び出しながら喜んで回り続ける様子を表します。 dataReceived のそれぞれの呼び出しはある特定の PoetryProtocol クラスのインスタンスに適用されます。 そして、みっつの詩をダウンロードしていますから、みっつのインスタンスがあります (接続もみっつあります)。

これらの Protocol インスタンスの「ひとつ」の観点から、この図について考えてみましょう。 それぞれのプロトコルはひとつの接続 (つまり、ひとつの詩) と一緒のことしか考えられていないことを思い出してください。 このインスタンスはメソッド呼び出しの一連の流れ (stream) を「目撃」し、それぞれのメソッドは詩の次の部分を bearing します。 こんな感じです。

dataReceived(self, "When I have fears")
dataReceived(self, " that I may cease to be")
dataReceived(self, "Before my pen has glea")
dataReceived(self, "n'd my teeming brain")
...

厳密に言えばこれは実際の Python ループではありませんが、次のように概念として表現できます。

for data in poetry_stream(): # pseudo-code
    dataReceived(data)

図41で、このコールバックループ (“callback loop”) をはっきりさせましょう。

_images/p20_callback-loop.png

図41:仮想的なコールバックのループ

繰り返しますが、これは for ループや while ループではありません。 詩のクライアントにおける重要な Python のループは reactor だけです。 しかし、それぞれのプロトコルを仮想的なループだと考えることもできます。特定の詩がやってくる度にくるくる回るのです。 この考えだと、図42のようにクライアントの全体像をもう一度想像できます。

_images/p20_reactor-3.png

図42:仮想的なループを回す reactor

この図ではひとつの大きなループ (reactor) と三つの仮想的なループ (個別の詩のプロトコルインスタンス) があります。 大きなループは回り続け、そうすることによって、連動ギアの集合のように仮想的なループを回し続けます。

Erlang へ

Erlang は Python のように、汎用的な動的型付けのプログラミング言語です。元々は80年代に作られました。 Python とは異なり、Erlang はオブジェクト指向というよりは関数型で、 Prolog を連想させる構文を持ちます。Prolog は Erlang が元々実装された言語です。 Erlang は高い信頼性を持つ分散電話通信システムを構築するために設計されましたので、充実したネットワークサポートを持ちます。

Erlang の最も独特な機能のひとつは、軽量プロセスを含んだ並列モデル - concurrency model - です。 Erlang のプロセスは OS のプロセスでもスレッドでもありません。 むしろ、独自のスタックを持つ Erlang ランタイムの中で独立して動作する関数です。 Erlang のプロセスは状態を共有できません (Erlang は関数型のプログラミング言語ですから、ほとんどのデータ型は変更不可能 - immutable - です) から軽量スレッドではありません。 Erlang のプロセスはメッセージを送ることによってのみ他の Erlang プロセスとやり取りできます。 そしてメッセージはいつも、少なくとも概念的には、コピーされますが決して共有されません。

このため、 Erlang プログラムは図43のように見えます。

_images/p20_erlang-11.png

図43:3つのプロセスがある Erlang プログラム

Python におけるオブジェクトのように Erlang におけるプロセスは第一級コンストラクト - first-class consutructs - ですから、この図では個別のプロセスは実際に存在 (“real”) します。 一方でランタイムは仮想的なもの (“virtual”) です。存在しないからではなく、必ずしも単純なループではないからです。 Erlang ランタイムはマルチスレッドかもしれませんし、full-blown プログラミング言語を実装できるように、非同期 I/O を扱う部分に多くの責任を持ちます。 さらに、言語のランタイムは全くもって追加コンストラクトではありません。 Twisted における reactor のように、 Erlang がプロセスとコードを実行するメディアだからです。

Erlang プログラムのより良い描き方は図44のようになるかもしれません。

_images/p20_erlang-2.png

図44:いくつかのプロセスがある Erlang プログラム

もちろん Erlang ランタイムは非同期 I/O とひとつ以上の select ループを使いません。Erlang は膨大なプロセスを生成できるようにしてくれるからです。 巨大な Erlang プログラムは何十、何百、何千という Erlang プロセスを開始させますので、実際の OS スレッドにそれぞれを割り当てるなんてことは問題外です。 Erlang が複数のプロセスに入出力操作を許可し、その入出力がブロックしても他のプロセスが実行できるようなら、非同期入出力が必要になりますよね。

Erlang プログラムの図では、コールバックによってくるくる回っているのではなく、それぞれのプロセスが自力で (訳注: “under its own power”) 動いていることに注意してください。 ここがとても大事な部分です。 Erlang ランタイムの仕組みに組み込まれた reactor のジョブがあると、コールバックはもはや中心的な役割を持ちません。 Twisted ではコールバックを使うことによって解決された問題は、Erlang では非同期メッセージをプロセスから他のプロセスへ送信することで解決します。

Erlang による詩のクライアント

Erlang による詩のクライアントを見ていきましょう。 Twisted でやってきたようにゆっくりと構築していくのではなく、一気に動くバージョンを扱います。 繰り返しになりますが、これは完全な Erlang 入門を意図していません。 とはいえ、このエントリが興味をそそるなら、このパートの終わりでもっと深く学ぶために読むべき書籍を紹介します。

Erlang のクライアントは erlang-client-1/get-poetry にあります。 実行させるためには、もちろん Erlang をインストールする必要があります。 main 関数のコードはこのようになります。 これは、 Python クライアントでの main 関数と似た目的を果たします。

main([]) ->
    usage();

main(Args) ->
    Addresses = parse_args(Args),
    Main = self(),
    [erlang:spawn_monitor(fun () -> get_poetry(TaskNum, Addr, Main) end)
     || {TaskNum, Addr} <- enumerate(Addresses)],
    collect_poems(length(Addresses), []).

これまでに Prolog かそれと似たような言語を見たことがないと、 Erlang の構文はちょっと奇妙に見えるかもしれません。 しかし、Python について同じように言う人もいます。 メイン関数は、セミコロンによって分割されたふたつの clauses によって定義されています。 Erlang は引数マッチによって動かすべき clause を選びますので、最初の clause はコマンドライン引数を与えることなくクライアントを起動したときのみ実行され、ヘルプメッセージを出力します。 ふたつ目の clause がアクションの中心となります。

Erlang の関数における独立した文はコンマで分割され、すべての関数はピリオドで終わります。 ふたつ目の clause におけるそれぞれの行を順番にみていきましょう。 1行目はコマンドライン引数を処理し、その結果を変数に束縛します (Erlang における全ての変数は大文字から始めなくてはいけません - capitalized)。 2行目は、 現在実行中の Erlang プロセス (OS のプロセスではありません) のプロセス ID を取得するために、Erlang の self 関数を使っています。 これがメイン関数ですから、Python での __main__ モジュールと同じものだと考えてください。 3行目はもっとも興味深いですね。

[erlang:spawn_monitor(fun () -> get_poetry(TaskNum, Addr, Main) end)
     || {TaskNum, Addr} <- enumerate(Addresses)],

この statement は Erlang のリスト内包表記 - list comprehension - で、Python での構文に似ています。 新しい Erlang プロセスを spawning します。このプロセスは、接続する必要のある詩のサーバのそれぞれになります。 それぞれのプロセスは同じ関数 (get_poetry) を実行するでしょうが、サーバに固有の引数は別々です。 新しいプロセスが詩を送り返せるように (一般的にプロセスにメッセージを送るためには、その PID が必要になります)、メインプロセスの PID も渡します。

main の最後の一文で collect_poems 関数を呼び出します。この関数は詩がやってくることと get_poetry プロセスが終了することを待ちます。 他の関数についてももう少し見ていきますが、まずは Erlang の main 関数と、Twisted クライアントで 等価な main 関数を比べてみましょうか。

それでは Erlang の get_poetry 関数に目を通していきましょう。 get_poetry というスクリプトには実際にはふたつの関数があります。 Erlang では、関数は名前と arity の両方で識別されますので、スクリプトはふたつの別々の関数を含みます。 それぞれ三つと四つの引数を受け付ける get_poetry/3get_poetry/4 です。 get_poetry/3 は次のようになり、 main によって spawn されます。

get_poetry(Tasknum, Addr, Main) ->
    {Host, Port} = Addr,
    {ok, Socket} = gen_tcp:connect(Host, Port,
                                   [binary, {active, false}, {packet, 0}]),
    get_poetry(Tasknum, Socket, Main, []).

この関数は、Twisted クライアントの get_poetry と同じように、まずは TCP 接続を生成します。 しかし、そこで処理を戻すのではなく、 get_poetry/4 を呼び出すことでその TCP 接続を使い続けます。 get_poetry/4 は次の通りです。

get_poetry(Tasknum, Socket, Main, Packets) ->
    case gen_tcp:recv(Socket, 0) of
        {ok, Packet} ->
            io:format("Task ~w: got ~w bytes of poetry from ~s\n",
                      [Tasknum, size(Packet), peername(Socket)]),
            get_poetry(Tasknum, Socket, Main, [Packet|Packets]);
        {error, _} ->
            Main ! {poem, list_to_binary(lists:reverse(Packets))}
    end.

この Erlang 関数は Twisted クライアントでは PoetryProtocol が果たしていたことをやっています。Twisted 版ではブロックする関数呼び出しを使いますが、Erlang 版はそうではありません。 gen_tcp:recv 関数はソケットへのデータ到着 (もしくはソケットが閉じられること) を待ち受けます。どれだけ長くなろうとも。 しかし、Erlang での “blocking” 関数は、関数を実行しているプロセスをブロックするだけで、Erlang ランタイム全体をブロックするわけではありません。 この TCP ソケットは本当はブロックしているソケットではありません (ピュア Erlang コードでは、真にブロックするソケットを生成できません)。 こうしたそれぞれの Erlang ソケットのために、Erlang ランタイムの中のどこかに、ノンブロッキングモードに設定された「本物の」TCP ソケットがあり、select ループの一部として使われます。

しかし、Erlang プロセスこれらに関して何も知りません。 ただただデータが届くのを待ち、もしもブロックするなら、他の Erlang プロセスが動くことになります。 プロセスは決してブロックしなくとも、Erlang ランタイムはいつでも実行プロセスを自由に切り替えられます。 言い換えると、Erlang は 非協調並行モデル - non-cooperative concurrency model - を持ちます。

詩の一部を受け取った後に、 get_poetry/4 が再帰的に自分自身を呼び出して実行することに注意しましょう。 imperative な言語のプログラマにとっては、メモリを使い果たしてしまう (out of memory) レシピのように見えるかもしれませんね。 しかし Erlang コンパイラーは末尾呼び出し (“tail” calls - 関数の最後にある関数呼び出し) をループに最適化できます。 このことは、Erlang と Twisted クライアントの間に横たわるもうひとつの興味深い点をハイライトしてくれます。 Twisted クライアントでは、reactor が何度も繰り返し同じ関数 (dataReceived) を呼び出すことによって “virtual” ループが生成されました。 Erlang クライアントでは、 tail-call optimization を使って、自分自身 (themselves) を繰り返し呼び出すことによって実行中の “real” プロセス (get_poetry/4) がループを形成します。 どうでしょうか。

接続が閉じられると、 get_poetry が最後にすべきはメインプロセスへの詩の送信です。 また、それ以上にすべきことが何もなくなるように、 get_poetry が実行しているプロセスを終了させます。

Erlang クライアントで残る主要な関数は、 collect_poems です。

collect_poems(0, Poems) ->
    [io:format("~s\n", [P]) || P <- Poems];
collect_poems(N, Poems) ->
    receive
        {'DOWN', _, _, _, _} ->
            collect_poems(N-1, Poems);
        {poem, Poem} ->
            collect_poems(N, [Poem|Poems])
    end.

この関数はメインプロセスによって実行され、 get_poetry のように、自分自身を再帰的にループします。 またブロックもします。 receive 文は与えられたパターンにマッチするメッセージが届くのを待つようにプロセスに伝えます。 受け取ったら「メールボックス」 (“mailbox”) からメッセージを展開します。

collect_poems 関数は二種類のメッセージを待ちます。詩と “DOWN” 通知です。 後者は、 get_poetry プロセスのひとつが何らかの理由で死んでしまったときにメインプロセスに送信されるメッセージです (これは spawn_monitor の一部である monitor です)。 DOWN メッセージを数えることによって、全ての詩を受け取ることが完了したときが分かります。 前者は、 get_poetry プロセスのひとつからのひとつの完全な詩を含んでいるメッセージです。

よし、Erlang クライアントを動かしてみましょう。 まずはゆっくりした詩のサーバ (slow poetry server) を三つ立ち上げます。

python blocking-server/slowpoetry.py --port 10001 poetry/fascination.txt
python blocking-server/slowpoetry.py --port 10002 poetry/science.txt
python blocking-server/slowpoetry.py --port 10003 poetry/ecstasy.txt --num-bytes 30

ようやく Erlang クライアントを実行できます。これは Python クライアントと似たコマンドライン構文を持ちます。 Linux か UNIX-like なシステムならクライアントを直接実行できます (Erlang がインストールされていて、 PATH が有効だと仮定しています)。 Windows ではおそらく、Erlang クライアントへのパスを最初の引数として (残りは Erlang クライアント自身への引数です)、 escript プログラムを実行する必要があるでしょう。

./erlang-client-1/get-poetry 10001 10002 10003

実行させてみるとこのような出力が見えるはずです。

Task 3: got 30 bytes of poetry from 127:0:0:1:10003
Task 2: got 10 bytes of poetry from 127:0:0:1:10002
Task 1: got 10 bytes of poetry from 127:0:0:1:10001
...

これは以前の Python クライアントの出力そのものです。受け取った詩の断片を出力させます。 すべての詩を受け取ると、クライアントはそれぞれの完全な文字列を出力するでしょう。 クライアントは、送信すべき詩が存在するか否かに従って全てのサーバに対して接続をあちこちと切り替えていることに注意しましょう。

図45は Erlang クライアントのプロセス構造を図示してくれます。

_images/p20_erlang-3.png

図45:Erlang での詩のクライアント

この図はみっつの get_poetry プロセス (サーバごとにひとつです) と、ひとつのメインプロセスを表します。 詩のプロセスからメインプロセスへ流れるメッセージも見えますね。

それでは、これらのサーバのひとつがダウンしたら何が起こるでしょうか? やってみましょう。

./erlang-client-1/get-poetry 10001 10005

上のコマンドは有効なポート (前述の詩のサーバは全て動作し続けていると仮定) と有効でないポート (10005番ポートを使っているサーバは存在しないと仮定) を含みます。 すると、このような出力が見られますね。

Task 1: got 10 bytes of poetry from 127:0:0:1:10001

=ERROR REPORT==== 25-Sep-2010::21:02:10 ===
Error in process <0.33.0> with exit value: {{badmatch,{error,econnrefused}},[{erl_eval,expr,3}]}

Task 1: got 10 bytes of poetry from 127:0:0:1:10001
Task 1: got 10 bytes of poetry from 127:0:0:1:10001
...

時折、クライアントはアクティブなサーバから詩をダウンロードし終えて、詩を出力してから終了します。 それでは、 main 関数が両方のプロセスが完了したことを知っていたらどうなるでしょうか? そのエラーメッセージが clue です。 get_poetry がサーバへの接続を試みて、期待する値 ({ok, Socket}) ではなく接続拒否のエラーを受け取ったときに、このエラーが発生します。 Erlang の “assignment” 文は実際にはパターンマッチ操作であるため、結果となる例外は badmatch と呼ばれます。

Erlang プロセス内で処理されていない例外はプロセスをクラッシュさせます。 これは、プロセスが停止し、そのプロセスに関する全てのリソースが回収 - garbage collected - されたことを意味します。 しかし、これらのプロセスが何らかの理由で動作を終了したら、 main プロセス、これはすべての get_poetry プロセスを監視しています、は DOWN メッセージを受け取るでしょう。 というわけで、クライアントは永遠に実行を続けるのではなく、終了します。

議論

Twisted と Erlang クライアントの間にある共通項を押さえておきましょう。

  • どちらのクライアントも一度に全ての詩のサーバに接続 (あるいは接続しようと) します。

  • どちらのクライアントも、どのサーバが運んできたかに関わらず、データがやってくるとすぐに受け取ります。

  • どちらのクライアントも少しずつ詩を処理しますので、そこまでに受け取ってきた詩の断片を保存しておく必要があります。

  • どちらのクライアントも、ある特定のサーバに対するすべての仕事を処理するために、”object” (Python オブジェクトか Erlang プロセスのどちらか) を生成します。

  • どちらのクライアントも、特定のダウンロードが成功したか失敗したかに関わらず、全ての詩を処理し終えたときを注意深く決定しなくてはなりません。

最後に、どちらのクライアントでも main 関数は非同期に詩とタスク完了 (“task done”) 通知を受信します。 Twisted クライアントでは、この情報は Deferred によって伝達されます。一方、 Erlang クライアントはプロセス間でメッセージを受け取ります。

戦略全体とコード構造の両方において、両方のクライアントにどれほどの類似点があるかに注意しましょう。 一方ではオブジェクト、遅延オブジェクト、それからコールバックを使い、もう一方ではプロセスとメッセージを使いますので、メカニズムはちょっと違います。 しかし、高レベルでのメンタルモデルはとても似ています。 双方に慣れ親しんでしまえば、どちらかからもう一方に移動するのはとても簡単なことでしょう。

Erlang クライアントでは reactor パターンでさえ非常に小型化された形式で再度出現します。 詩のクライアントにおけるそれぞれの Erlang プロセスは時々再帰的なループになります。

  • 何かが起きることを待ち (詩の断片が届く、とか、詩が転送される、とか、もうひとつのプロセスが完了する、など)、

  • いくつかの適切なアクションを実行します。

Erlang プログラムを、小さな reactor がたくさん集まったものだと考えることもできます。 それぞれが spinning し、時々小さな reactor にメッセージを送るのです (他のイベントと同じようにそのメッセージを処理するでしょう)。

もしも Erlang についてより深く学ぼうと思ったら、コールバックを見える化しましょう。 Erlang の gen_server プロセスは、固定数のコールバック関数を提供することによってインスタンス化 (“instantiate”) する、汎用的な reactor ループです。 これは、Erlang システムのそこここで繰り返し現れるパターンです。

Twisted を学んだ後で、もしも Erlang をやってみようと思ったなら、慣れ親しんだメンタルテリトリー (familiar mental territory) にあると気付くでしょう。

さらに読むべきもの

このパートでは、Twisted と Erlang で共通したことに焦点を当ててきました。しかしもちろん、違う部分もたくさんあります。 Erlang 独特の機能として、エラー処理のアプローチがあります。 巨大な Erlang プログラムは、上位の枝分かれ (higher branches) にスーパーバイザー (“supervisors”) を持ち、葉にワーカー (“workers”) を持つ、プロセスの木として構成されます。 もしもワーカープロセスがクラッシュすると、スーパーバイザープロセスが気付き、いくつかのアクションを引き継ぐでしょう (典型的には、失敗したワーカーを再起動させます)。

Erlang についてもっと学習したくなったらツイテますね。 いくつかの Erlang 本が最近になって出版されたか、まもなく出版 (1) されます。

  • Programming Erlang — Erlang 開発者のひとりによって書かれた書籍です。言語への素晴らしい入門編です。

  • Erlang Programming — Armstrong の書籍を補完し、いくつかの主要な領域についてより深く記述されています。

  • Erlang and OTP in Action — この書籍はまだ発売されていませんが、手元に届くのが待ちきれません。 上のふたつの本は OTP には言及していません。OTP は Erlang で大きなアプリケーションを構築するためのフレームワークです。 ちなみに、著者のふたりは私の友達です。

1

2010年12月に出版されました。

Erlang に関してはこのくらいにしておきましょう。 “パート21: おろそかにならないようにゆっくりと: Twisted と Haskell” では Haskell を見ていきます。Python とも Erlang とも大いに異なる雰囲気を持つ、もうひとつの関数型言語です。 言うまでもありませんが、いくつかの共通点を見出していくことになるでしょう。

おすすめの練習問題

  1. Erlang と Python クライアントを見比べてみて、似ている部分と異なる部分を見分けましょう。 どのようにエラー (詩のサーバに接続失敗したように) を処理しているでしょうか?

  2. 受信した詩の部分部分を出力しないように Erlang クライアントを単純化してください (タスク番号を追跡する必要もありませんね)。

  3. それぞれの詩をダウンロードする時間を計測するように Erlang クライアントを修正してください。

  4. コマンドラインで与えられた順番と同じ順番で詩を出力するように Erlang クライアントを修正してください。

  5. 詩のサーバに接続できないときに、もっと可読性の高いエラーメッセージを表示するよう Erlang クライアントを修正してください。

  6. Twisted を使って実装した詩のサーバの Erlang バージョンを記述してください。