wiki:PythonStacklessPythonSocket

Stackless PythonでUDPなサーバを作る

 Stackless Pythonおすすめですよ。 どういうときにおすすめなのか判らないですよね。 たとえば、ウェブサーバのように複数の場所からの複数の要求に応じて同時並行的に処理をするソフトウェアを作るときです。

サーバ処理の方式

こういったソフトウェアを作るときは、これまでは、以下のような方法で処理していました。

  1. 複数の要求を複数のプロセスに割り振って処理させる方式。
    1. 要求がある度に、新たなプロセスを生成する。
    2. あらかじめ一定数のプロセスを生成しておき、手の空いているプロセスが処理する。
  2. 複数の要求を複数のスレッドに割り振って処理させる方式。
  3. Unixの「selectシステムコール(またはpoll)」を使って、データ駆動的に多重化する。

たとえば、Unixのinetdは1.1.の方式、Apacheの通常の構成は1.2.の方式です。 ウィンドウズのIIS(「Internet Information Services」)は、2.の方式です。 これに対して、3.の方式で有名なのは、X Windowです。ソース読んだことありますか? 僕はこれに影響されて自社フレームワークを作り、当社のRADIUSサーバで使用しました。 当社のRADIUSサーバのうち、ある製品にはHTTPサーバも組み込みました。 1個のプロセス、マルチスレッド無しでRADIUSのUDPパケットの送受信と、ウェブベースのGUIとを同時に処理するという凝ったことをやりました。

歴史的なことを言えば、古くには1.1.の方式しかありませんでした。 そもそも、Unixは、比較的単純なカーネルを特徴としていて、プロセス生成のコストも比較的小さいものとして扱われてきました。 1個1個単純な機能を持つ小さな実行ファイルを複数組み合わせて目的を達するというUnixの考え方はここから来ています。 確かに、大型機のOSと比べたらプロセス生成のコストは小さいのでしょうけれど、要求の頻度が高いとコストが無視できません。 じゃあ、要求がある度にプロセスを生成するのでなくて、1個のプロセスで複数の要求に応えればいいだろうというのが1.2.の方式です。 あらかじめ一定個数のプロセスを生成しておいて、それらに均等に要求を割り振って処理させ、処理が終わったら待機させることの繰り返しです。

1.2.の方式である程度の性能は稼げるのですが、異なるプロセスで処理が行われることが好ましくない場合があります。 すぐに思いつくのは、メモリ空間を共有していないということです。 複数の要求に対して、互いに協調したり情報を共有しながら処理を行うには、都合が悪いです。 X Windowがまさにそれです。画面のフレームバッファなどを共有する必要があるから。そのため、3.の方式が採用されました。 歴史学者ではないのでうろ覚えですが、X Windowが開発されたのは、selectシステムコールが出現したすぐ後だったように思います。 X Windowの方式は、当時の最先端だったように思います。

3.の方式の欠点は、「データ駆動的」であるということです。 1個のプロセスの中心にselectシステムコールの実行処理があり、CPUはこの周りをぐるぐる回り続けます。 どっかからひとかたまりのデータが届くと、それを読み込んで処理して応答するということを繰り返します。 問題は、TCPのストリームからのデータです。CPUが、1個のストリームにかかりっきりになることはできません。 すでに届いているデータだけを読んで、届いているデータが無くなったら、readで待ち続けるのではなくて、selectに戻る必要があります。 加えて言うなら、ネットワークへのデータの送信、ファイルの読み書きさえも、「待たされる可能性のある処理」は全て「待たずに終わる」ことが必要。 そのため、たとえばHTTPの処理を「まっすぐに」は書くことができません。 HTTPの要求メッセージの後半が届くのが、なんらかの都合で遅れているようなら、途中経過を保持したまますぐにselectに戻る必要がある。 正直、つらいです。 上記のRADIUSサーバ内蔵のHTTPサーバや、付随してDNSクライアントを書いたときは、頭がおかしくなりそうでした。

この欠点を補うのが2.の、マルチスレッドの方式です。 スレッドはすてきです。なんでもまっすぐに書くことができます。 プログラミングで注意しなきゃいけないことがちょっと増えるけれど、得られる物はそれを補って余りあります。 でも、マルチスレッドも万能ではありません。 たとえば、同時並行に処理すべき要求の個数が1000個とか10000個とかになったらどうしましょう。 こんな数のスレッドを起動することはできません。

Stackless Python

ここでようやく、Stackless Pythonの登場です。 「Stackless Python」のことを、ときおり「Stackless」と呼ぶことがありますが、同じです。気にしないでください。 さて、「スタックレス」という名前に惑わされてはいけません。 スタックが無いから再帰呼び出しがたくさんできる!とかで話が終わってしまいます。 Stacklessのキーポイントは、軽量な擬似的スレッドにあります。 Stacklessの用語では「タスク」と呼ばれています。 見た目はただのオブジェクトにしか見えません。スレッドとは異なり、スタックを消費しません。 実行途中の状態は、どうやら、そのオブジェクト内部に全て記憶されているようです。 スタックを消費しないため、たくさんたくさん生成することができます。 まっすぐに処理を記述しながらも、いつでも途中で気絶してCPUを明け渡し、他のタスクにCPUを使わせることができます。 まるで、CPUにとっての ワームホールであるかのようです。 このタスクは、シリアライズしてファイルに保存することさえできるのです。処理の途中の状態で!  また、タスク間でデータを受け渡す機能も提供されています。 もちろん、読み出し側のタスクがデータを待っている間、CPUが読み出し側のタスクで待ちぼうけを喰らうことはありません。 この間、当然ながら、読み出し側のタスクにはCPUを割り当てず、他のタスクだけにCPUを割り当てるのです。

この疑似スレッドをうまく使うと、上記の3.の方式で「データ駆動」でありながら、個々の処理ロジックはまっすぐに記述することができます。

ようやく本題まで来ました。

実装例

Stacklessを使って、UDPを受信するサーバを作ります。

まず、Stackless用のsocketモジュールが必要です。 Stackless Pythonをビルドしただけでは、ネットワークやファイルのI/Oの部分は普通のPythonと変わりがありません。 つまり、I/O待ちになったら、CPUを抱えたまま寝てしまいます。これでは、Stacklessの意味がありません。 I/O待ちで寝てしまうのを防ぐには、Stackless用のI/Oモジュールが必要です。 Stackless用のsocketモジュールは、 Stacklessのコーディング例のページからリンクされている、 stacklesssocket.pyです。 これをPython付属のsocketモジュールの代わりに使うだけで、Stacklessを生かすことができます。 しかし、残念ながら、どうも動きがおかしいです。特に、UDPではうまく動きません。 recvfrom()をすると、指定したバッファ長が満たされるまで、複数のUDPパケットを連結してしまいます! そりゃないよ。 うーん。これを作った人は、あまりUDPはいじらないようです。

僕がこの問題を修正したstacklesssocket.pyはここにあります。

上記のページによれば、2008年5月にstacklesssocket.pyを修正してくれたようです。僕のパッチが役に立ったみたい。

で、これを使って実装した、単純なエコーサーバがこれです。

いやー。すばらしく簡単すぎます。でも、これをもしもTwistedで書いたら、リアクターだの何だの、だるいこと間違い有りません。 Twistedは、デバッグがつらいしね。

つづく