Twisted Intro

パート2: ゆったりした詩と世紀末

«  パート1: さあ、はじめましょう   ::   Contents   ::   パート3: 目を向けてみましょう  »

パート2: ゆったりした詩と世紀末

このパートでは”パート1: さあ、はじめましょう“で始めた入門の続きを進めていきます。もし読んでいるなら、ようこそ戻ってきてくれましたね。いよいよ自分の手を動かしてコードを書いていきましょう。でもまずはちょっと脱線して、想定していることを共有しておきましょう。

想定している読者

Python で同期プログラムを書く基礎知識を身につけていて、Python のソケットプログラミングについて少しは知っているものと考えています。もしソケットを使ったことがなければ、とりあえず ソケットモジュールのドキュメント 、特に最後の方のコード例、に目を通しておくと良いでしょう。Python を使ったことがないと、これ以降はボヤッとしたものになってしまうでしょう。

想定している読者のコンピュータ

私が Twisted を使ってきたのは主として Linux 上であり、コード例も Linux で作りました。意図的に Linux べったりのコードにしているつもりはありませんが、Linux や Unix のような (Mac OSX や FreeBSD など) システムでしか動作しないものがあるかもしれません。Windows はちょっと変わってますので、あなたが Windows 上で作業していても気持ち以外は助けになれません。

マシンには比較的最近の PythonTwisted をインストールしてあるものとします。例で示すコードは Python 2.5 と Twisted 8.2.0 で開発しました。

すべての例はネットワーク越しで設定できますが、単一のマシンで動作させられます。非同期プログラミングの基本的な仕組みを学習するためには単一のコンピュータが良いでしょう。

コード例の入手

コード例は ziptar か、あるいは 私の公開 git レポジトリclone する ことで入手できます。 git か git レポジトリを読めるような版管理システムを使っているのなら、こちらの方法をお勧めします。将来的にはコード例を更新していきますし、あなたも最新の状態を保ちやすくなりますからね。おまけに図を作成するために使った SVG ファイルもあります。次の git コマンドでレポジトリを clone してください。

git clone git://github.com/jdavisp3/twisted-intro.git

以降では、最新のコード例があり、最上位ディレクトリを開いたシェルが複数あるものとします。(その内のひとつは README を開いておいてください)

のんびりした詩

CPU はネットワークよりはるかに高速ですが、それでも、ほとんどのネットワークはあなたの頭がものごとを考えるよりはかなり高速ですし、少なくとも眼で追えるよりは高速です。このため、「CPU から見た」ネットワークの遅延を確認することは難しい問題になりえます。マシンが一台しかなく、バイト列が ループバックインターフェイス を最高速で駆け抜けていくような場合は特にそうです。人間が判別できるような人為的な遅延を持った、遅いサーバが必要です。サーバは何らかのことを提供しなくてはいけませんから、詩を提供することにしましょう。コード例には「 poetry 」というサブディレクトリがあります。 John DonneW.B. YeatsEdgar Allen Poe のそれぞれの詩をひとつずつ含んでいます。もちろんあなた好みの詩に入れ替えてもらって構いませんよ。

基本的なのんびりした詩のサーバは blocking-server/slowpoetry.py で実装しています。次のようにしてインスタンスを起動できます。

python blocking-server/slowpoetry.py poetry/ecstasy.txt

このコマンドは John Donne の詩である「Ecstasy」を送り出すブロッキングサーバを開始します。ちょっとブロッキングサーバのソースコードをのぞいてみましょう。見れば分かるように、Twisted を使っておらず、基本的な Python のソケット操作だけです。一度にある程度のバイト数しか送り出さず、それらの間隔には同じだけの遅延があります。デフォルトでは 0.1 秒ごとに 10 バイトを送り出しますが、コマンドラインの --num-bytes--delay オプションでパラメータを変更できます。たとえば、5 秒ごとに 50 バイトを送るためには次のようにします。

python blocking-server/slowpoetry.py --num-bytes 50 --delay 5 poetry/ecstasy.txt

サーバは、起動するときに待ち受けポート番号を出力します。デフォルトではマシンで利用可能なランダムなポートを使います。設定を変えて起動する場合、クライアントのコマンドを調整する必要がないように同じポート番号を使いたくなるでしょう。次のようにすると特定のポート番号を指定できます。

python blocking-server/slowpoetry.py --port 10000 poetry/ecstasy.txt

netcat [*] プログラムを使える場合は次のようにテストできます。

netcat localhost 10000
[*]訳注: コマンド名が netcat ではなく nc の処理系もあるかもしれません。

サーバが動作していれば、詩が画面にゆっくりと流れてくるでしょう。スゴイ!(訳注:Ecstasy! と掛けてるから英語だとおもしろい、というジョーク) また、サーバがバイトを送出するごとに一行ずつ出力していることに気付くでしょう。完全に詩を送りきってしまうと、サーバは接続を切断します。

デフォルトでは、サーバは自分のマシンの「ループバック」インターフェイスを listen しているだけです。もし別のマシンからサーバに接続したい場合は、 --iface オプションで listen するインターフェイスを指定できます。

サーバがそれぞれの詩をゆっくりと送り出すだけでなく、ソースコードを読むと、サーバがあるクライアントに詩を送っている間は他の全てのクライアントはそのクライアントに詩を送りきるまで待たなくてはならないことがお分かりでしょうか。本当にのんびりとしたサーバですので、学習用以外には特に役立たないでしょう。

ホントでしょうか? 他方で、 Peak Oil のもっと悲観的な人たちが正しく、実世界は世界的なエネルギー危機と地球温暖化に向かっているとすれば、 遠からぬ未来にはたぶん、帯域を食わず低電力の poetry server が私たちに必要なものになるかもしれません。 想像してみてください。長らく、自分が満足するような庭の手入れに精を出し、自分自身の洋服をつくり、地域の自治会に奉仕し、 世紀末後の荒廃した状態を転々とする非常にデリケートな無気力人間 (訳注:zombie。ウィルスに感染したコンピュータのことかもしれないし、得体の知れないものかも) を撃退しながら過ごしてきた後に、 あなたの発生器 (訳注:詩を送り出すサーバのこと) を開始して、消失してしまった文明から高度な文化の数行をダウンロードできるかもしれません。このときこそが私たちのちっぽけなサーバがその意義を発揮するときなのです。

ブロッキングクライアント

コード例には複数のサーバから次々に詩を取得できるブロッキングクライアントもあります。パート1の”図1:同期モデル“で示したように、クライアントに三つのタスクを実行させてみましょう。まずは三つのサーバを実行させ、別々の詩を送り出させます。三つの別々のターミナルウィンドウでこれらのコマンドを実行してください。

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

あなたのシステムで上記のポート番号がすでに使われているようなら違うポート番号にしても構いません。最初のサーバはデフォルトの 10 バイトではなく 30 バイトずつにしていることに注意してください。この詩は他のに比べて三倍くらいの長さがあるからです。こうしておくと、だいたい同じくらいのタイミングで終わるようになります。

それでは、詩を取得するために blocking-client/get-poetry.py のブロッキングクライアントを使いましょう。次のようにしてクライアントを実行します。

python blocking-client/get-poetry.py 10000 10001 10002

サーバの設定に合わせてポート番号を変えてください。これはブロッキングクライアントなので、完全な詩を受け取るまで待ち、次の詩が始めるのを待ちながら、それぞれのポート番号から交互に詩をダウンロードするでしょう。詩を出力する代わりに、ブロッキングクライアントは以下の出力を生成します。

Task 1: get poetry from: 127.0.0.1:10000
Task 1: got 3003 bytes of poetry from 127.0.0.1:10000 in 0:00:10.126361
Task 2: get poetry from: 127.0.0.1:10001
Task 2: got 623 bytes of poetry from 127.0.0.1:10001 in 0:00:06.321777
Task 3: get poetry from: 127.0.0.1:10002
Task 3: got 653 bytes of poetry from 127.0.0.1:10002 in 0:00:06.617523
Got 3 poems in 0:00:23.065661

基本的にこれは”図1:同期モデル“のテキスト版で、それぞれのタスクはひとつの詩をダウンロードすることです。あなたの環境ではちょっと違うかもしれませんし、サーバのタイミングパラメータを変更すれば変わってくるでしょう。パラメータを変更してみて、ダウンロード時間への影響を確認してみてください。

ブロッキングサーバとクライアントのソースコードに目を通して、ネットワークデータを送受信する部分がソースコードのどの辺にあるは分かったでしょうか。

非同期クライアント

それでは、Twisted を使わないで書いた簡単な非同期クライアントを見ていきましょう。とりあえず動かしてみましょう。さっきと同じポートで三つのサーバが動作しているものとします。先ほど起動したものが動いていればそのままにしておいてください。非同期クライアント (async-client/get-poetry.py にあります) は次のようにして実行します。

python async-client/get-poetry.py 10000 10001 10002

こんな感じの出力になるでしょう。

Task 1: got 30 bytes of poetry from 127.0.0.1:10000
Task 2: got 10 bytes of poetry from 127.0.0.1:10001
Task 3: got 10 bytes of poetry from 127.0.0.1:10002
Task 1: got 30 bytes of poetry from 127.0.0.1:10000
Task 2: got 10 bytes of poetry from 127.0.0.1:10001
...
Task 1: 3003 bytes of poetry
Task 2: 623 bytes of poetry
Task 3: 653 bytes of poetry
Got 3 poems in 0:00:10.133169

今回は出力がちょっと長くなっています。非同期クライアントはサーバからダウンロードするごとに一行を出力しており、のんびりした詩のサーバはちょっとずつバイト列を送り出しているためです。パート1の”図3:非同期モデル“のように、個別のタスクが一緒くたにまとめられていることに注意してください。

どうやって非同期クライアントが、速いサーバに遅れないままで、遅いサーバのスピードに自動的に合わせるのかを確認するために、サーバの遅延設定を変えて (たとえば、あるサーバを他のサーバより遅くしてみる、とか) みてください。これこそが非同期の醍醐味です。

全ての詩を取得するのに (上記のサーバ設定の場合は)、非同期クライアントは 10 秒くらいで終了するのに、同期クライアントは 23 秒くらいかかることにも注意してください。パート1の”図3:非同期モデル“と”図4:同期プログラミングでのブロッキング“の違いを思い出してください。ブロックする時間があまりありませんので、非同期クライアントは全体として短い時間で全ての詩をダウンロードできるのです。確かに、非同期クライアントでもブロックは発生しています。ゆったりしたサーバは遅いのです。単に非同期クライアントは全てのサーバへの対応を切り替えているため、ブロッキングクライアントに比べてブロックされる時間に多くを費やさないだけです。

技術的なことを言えば、非同期クライアントはブロッキング操作を実行しています。 標準出力のファイルディスクリプタに print 文で書き出しています。ここの例ではこれは問題になりません。 print 文による出力を常に受け付けてくれる端末のローカルなマシンでは実際にはブロックしないでしょうし、サーバの遅さに比べれば素早く実行されます。 しかし、プログラムをパイプライン処理の一部分にしてその中で非同期に処理したい場合は、標準入出力のための非同期入出力を使う必要があるでしょう。 Twisted はこの機能をサポートしていますが、全体を単純化しておくために print 文を使います。この先の Twisted を用いたプログラムでもそうします。

もっと詳しく

それでは、非同期クライアントのソースコードに目を通してみてください。非同期と同期での主要な違いに気をつけてください。

  1. 非同期クライアントは、一度にひとつのサーバに接続するのではなく、一斉に全てのサーバに接続します。
  2. setblocking(0) の呼び出しで、通信に使われるソケットオブジェクトはノンブロッキングモードになっています。
  3. select モジュールの select メソッドを使うことで、ソケットがなんらかのデータを受け取れるようになるまで待つようにしています。
  4. サーバからのデータを読み込むときは、ソケットがブロックするまでに読める程度しか読み込みませんし、読むべきデータのあるソケットに処理を移します (もしあれば)。これは、それぞれのサーバからその時点までに受信した詩の内容を管理し続けなくてはならないことを意味します。

非同期クライアントで核となる部分は、 get_poetry 関数の最上位のループです。このループは次のステップに分解できます。

  1. select を使い、ひとつ以上のソケットが読むべきデータを持つようになるまで、全ての有効なソケットを待ち受けます。
  2. 読むべきデータのあるそれぞれのソケットに対して、データを読み込みます。ただし、その時に有効なだけのデータしか読み込みません。 ブロックしてはいけません
  3. 全てのソケットが閉じられるまで繰り返します。

同期クライアントにも (main 関数の中に) ループはありましたが、それぞれの繰り返し処理の中でひとつの詩を完全にダウンロードしていました。非同期クライアントにおける繰り返し処理では部分的にしかダウンロードしません。そして、ある繰り返し処理においてどの詩を扱っているか、どのくらいのデータ量を受信するかは分かりません。これらはすべてサーバの相対的なスピードとネットワークの状態にかかっています。どのソケットに対して処理するかを select に教えてもらい、ブロックしないようにそれぞれのソケットからデータを読み込むようにする外ありません。

同期クライアントがいつも決まった数のサーバ (たとえば三つ) と通信しているならば、外側のループは全くいらなくなり、 get_poetry 関数を順番に三回呼び出すだけでよくなります。しかし、非同期クライアントでは外側のループをなくすことはできません。非同期の良さを活かすために、全てのソケットを待ち受け、その時々の繰り返し処理で読み込めるだけのデータに対して処理を進めなくてはなりません。

イベントが発生するのを待ち受けそれを処理するループの使い方は、いわゆるデザインパターンにおける reactor pattern です。次の図5のように表せます。

_images/p02_reactor-1.png

図5:同期モデル

イベントを待って処理を行うので、ループは「reactor」です。イベントループともいわれます。reactive system は入出力を待つことが多いため、こうしたループは select loops とも呼ばれます。 select の呼び出しは入出力を待つために使われるからです。 select ループの中では、「イベント」とはソケットが読み込みか書き込みができるようになったときです。入出力を待ち受ける方法は select だけではないことに注意してください。単に古くからある方法 (それゆえに広く利用可能) というだけです。異なるオペレーティングシステムで利用可能で、 select と同じことができて (願わくば) よりよい性能をもたらしてくれる新しい API もいくつかあります。しかし、性能のことに目をつむればどれでも同じことです。ソケットの集合 (実際はファイルディスクリプタ) を受け取って、ひとつ以上が入出力の準備ができるまでブロックするのです。

select やその類を使って、ブロックせずにファイルディスクリプタの集合が入出力の準備ができているかを単に確かめることもできます。 この機能は reactive system がループの中で入出力を持たずに動作できるようにしてくれます。 しかし、reactive systems では全ての処理が入出力に抑制されてしまう場合がしばしばありますので、全てのファイルディスクリプタをブロックすることで CPU 資源を節約できます。

厳密な言い方をすれば、ここで示した非同期クライアントでのループは reactor pattern ではありません。ループのロジックが詩のサーバに特有である「ビジネスロジック」と分離されていないためです。全部ごっちゃになっています。reactor pattern の現実的な実装では、ループを次の機能を持つ抽象的なものに分けることになるでしょう。

  1. 入出力を監視したいファイルディスクリプタの集合を受け取ります。
  2. ファイルディスクリプタが入出力の準備ができたことを繰り返し知らせます。

そして、本当に良い reactor pattern の実装は次のような機能も持つでしょう。

  1. 異なるシステムで出現する全てのおかしな場合を扱います。
  2. reactor を最低限の努力で使えるようにしてくれる多くの嬉しい抽象化を提供します。
  3. 自由な発想で使える、よく知られたプロトコルの実装を提供します。

これがまさに Twisted です。堅牢でクロスプラットフォームな Reactor Pattern およびそれ以上のたくさんのものの実装です。”パート3: 目を向けてみましょう“では、Twisted 版の Get Poetry Now に向けて単純な Twisted のプラグラムをいくつか書いていきます。

おすすめの練習問題

  1. サーバの数や設定を変えて、ブロッキングの非同期クライアントでいくつかのタイミングを実験してみること。
  2. 非同期クライアントで詩の内容を返すような get_poetry 関数を提供できるでしょうか?理由は?
  3. 非同期クライアントで似たような方法で (それでも非同期に) 動く get_poetry 関数を欲しくなったときに、引数と戻り値はどのようなものになるでしょうか?

«  パート1: さあ、はじめましょう   ::   Contents   ::   パート3: 目を向けてみましょう  »