Winsock Programmer's FAQ
第7章 論説記事: どの I/O 戦略を使うべきか?

どの I/O 戦略を使うべきか?

Warren Young 著

Winsock には、いくつかの異なる通信のための慣習が存在し、それ ぞれに違ったメリットがあります。この時間の議題は、それぞれの利点 は何で、アプリケーションを作るときに最も意味をある方式をどのよう に選択すればよいか、ということです。選択肢には以下のものがありま す。

  • ブロック型ソ ケット - デフォルトでは Winsock 呼び出しはブロックします。 つまり、その処理が完了するか処理が失敗するまで戻ってきません。

  • 非ブロック 型ソケット - 非ブロック型ソケットに対する呼び出しは、 その処理が即座に完了できなかった場合でも、すぐ戻ってきます。 これによって、ネットワーク操作が終了するまでの間プログラムで は別のことをやっていることができるようになりますが、処理の要 求が終了したかどうかを判断するために、プログラムから繰り返し ポーリングしなくてはなり ません。

  • 非同期型 ソケット - これも非ブロック型ソケットですが、ポー リングする必要はありません。何か「興味のある」出来事が発 生したときに、プロトコルスタックが特定のウィンドウメッセー ジをプログラムに送信するのです。

  • select() - select() 関数呼び出しは、あるソケットの集合の中で何かイベントが発 生するまでスレッドをブロックするための方法の一つです。こ の方法は通常、非ブロック型ソケットに対してポーリングを行 わないようにするために使用されます。

  • イベントオブジェクト - この方法は WSAEventSelect() を使うもので、機構は select() の方法と同様ですが、若干効率が良い です。また、select() の方式は BSD ソケット がある全てのプラットフォームで動作しますが、この方式は Winsock があるプラットフォーム特有のものです。

  • オーバーラップ I/O - Winsock 2 における主要な機 能の一つに、ソケットが Win32 の統一 I/O 機構に取り込まれてい るというものがあります。特に、上記で述べてきた選択肢よりも本 質的に効率の良いオーバーラップ I/O がソケットに対しても使用 できるようになりました。

スレッドが出てくると話がさらにややこしくなります。スレッドと 一緒に使うと、上記のそれぞれのメカニズムが根本から変わってしまう からです。

「どのI/O戦略にするか」問題に対する答えを出そうとしてみると、 プログラムは大まかに数種類存在し、良いものは同じパターンに従って いるのだ、ということが明らかになってきます。そういったパターンや 実際の経験(私個人のものもあれば他の人から借りたものもあります)を もとに、私は以下のような経験則を導き出しました。これらの経験則は どれも絶対的な法律ではありませんし、単独では十分なものではありま せんし、ときにはそれぞれが相容れないものだったりします。二つの経 験則が相矛盾しているときは、あなたのアプリケーションにとってどち らがより重要で、どちらを無視するかを決定しなければなりません。た だし、これらの経験則を単純には無視しないように用心してください。 これらを破っても、あなたのプログラムに対して、特にこれといった結 果を生み出さないからです。ある経験則を無視するような習慣がついて しまっては、元も子もありません。

ここでの経験則は、互換性、次に速度、最後に機能性という観点で の順序で並んでいます。互換性が最初にあるのは、もしあるI/O戦略が、 あなたがサポートする必要のあるプラットフォームで動かないのであれ ば、それがいかに早くともあるいは便利であっても関係ないからです。 次が速度であるのは、性能に対する要求は決定が容易で、かつ重要とな ることが多いからです。機能性が最後なのは、互換性と速度の問題が決 まれば、あとはより本題に近い選択が必要になるからです。

経験則1: どのOSのサポートが必要かを決定し選択肢を限定する。

現在 Windows には、大きく分けて Win9x と WinNT の二つの種類が 存在します。私が「Win9x」と呼んでいるものには、現在 Windows 95、 Windows 98、Windows ME が含まれます。「WinNT」には、現在 Windows NT 4.0、Windows 2000、Windows XP が含まれます。これらの二種の Windows は、それぞれのシリーズの中で「上位互換性」を持っています。 例えば、ある機能が Windows NT 4.0 にあるとすれば、その機能は Windows 2000 でも Windows XP でも動作するだろう、ということです。 この文書では、これら二種のシリーズに含まれないオペレーティングシ ステムは全て別物として取り扱っています。つまり、Windows NT 3.x、 Win16、Windows CE、Unix、その他Windows 以外のシステムの Winsock で触れられているもの全て別物として 取り扱います。

ある場合には Unix をサポートする必要があるかも知れません。私 が「Unix」と言ったとき、BSD ソケットと POSIX スレッド("pthreads" とも言われる)を持つオペレーティングシステムを意味しています。こ の記事で触れる範囲においては、伝統的な Unix に加え Linux、MacOS X、QNX、BeOS なども全て「Unix」と捉えてもらって構いません。

想定するOSの範囲が広がると、I/O戦略の選択肢がいくつかの出てき ても不思議ではありません。これらの変種の多くは、BSD ソケットに対 して新しい Windows の機能を有効に利用できるようにした、Winsock の拡張機能です。

    Win9x WinCE WinNT 4+ WinNT 3.x Win16 Unix
  ブロック型ソケット 有り 有り 有り 有り 有り 有り
  非ブロック型ソケット 有り 有り 有り 有り 有り 有り
  非同期型ソケット 有り 無し 有り 有り 有り 無し
  イベントオブジェクト 有り 無し 有り 無し 無し 無し
  オーバーラップ I/O 有り1 無し 有り 無し 無し 無し2
  スレッド 有り 有り 有り 有り 無し 有り3
  1. Win9x ではオーバーラップ I/Oをカーネルレベルではサポート していません。Win9x 上でオーバーラップI/Oが動作しているのは、 この機能がAPIのレイヤでエミュレートされているからなのです(こ れは、少なくとも Winsock、ファイル、シリアル/パラレルポート の入出力に対して当てはまります)。この意味はつまり、Winsock 仕様で保証される範囲でのオーバーラップ I/O 機能のみを使って いるプログラムは Win9x でも動作しますが、もしたまたま WinNT 4 以降でのみ提供される機能を使ってしまうと、Win9x では動かな くなってしまう、ということです。その一つの例として、ソケット に対して ReadFile()を呼び出す、というのがありま す。これは NT 4以降では問題なく動きますが、Win9x では動きま せん。

  2. 単なる分割/集積I/O が必要なだけであれば、BSD ソケットで は readv()writev() がこの機能 を提供しています。標準的な Unix の機構には、Win32 のオーバー ラップI/Oと同様な効率性を提供するものはありません。ある種の Unix では aio_*() ファミリーの関数(非同期I/Oと 呼ばれていますが、Winsock の非同期I/Oと関連はありません)が提 供されていますが、現時点ではこの実装はあまり広まっていません。

  3. 現代の Unix では全て POSIX スレッドがサポートされていますが、 世の中にはまだ、貧相で非標準のスレッドあるいはスレッドさえも 持っていない、古い Unix マシンがたくさん存在しています。もし、 Windows と複数の種類の Unix 間で移植性があるようなコーディン グをしたいというのであれば、そのターゲットとなるプラットフォー ムを非常に注意して選ばないと、おそらくスレッドを使うことさえ できないでしょう。また、スレッドを使うプログラムを Windows と Unix の両者で動かす必要がある場合には、何らかのスレッド共 通化ライブラリを買ってくるか自分で書くかする必要があるでしょ う。両者のスレッドは、機能的には似ているものの、API は異なっ ているからです。

経験則2: 単純な非ブロック型ソケットは避けること。

単純な非ブロック型 ソケットが必要になることはまずありません。またその利点もあり ません。これはその非効率さゆえに、Windows プログラムにとっては貧 相なアーキティクチャの選択肢にしかなりえないのです(「単純な非ブ ロック型」という意味は、非ブロック型ソケットを使うときに、 "select" 関数の類、つまりselect()WSAAsyncSelect()WSAEventSelect()を一 緒に使っていない、という意味です)。

ソケットを非ブロック型に設定すると、ソケットに対する全ての Winsock 呼び出しは、何かを実行できたかできないかに関わらず、即座 にリターンします。これは、ネットワークが混雑しているときでもプロ グラムには別の仕事をさせることができるので便利なのです。

ほとんどのプログラムは、常に何かをしていなければならな い、というわけではありません。ユーザやネットワーク、その他遅いも のからの入力を待っているのが通常です。まさにこのために Winsock が提供しているものが "select" 関数です。上記三つの関数は、動作の 仕組みはそれぞれで異なりますが、これらをプログラム中に置くことで、 ネットワーク入出力を待っている間CPU時間を無駄にすることが無いよ うに sleep させることもできるし、プログラムで使っているソケット に何かかが起こるまで、GUIイベントループに戻るようにすることもで きます。

経験則3: select()は避ける。

上記で私は、select() 関数は、非ブロック型ソケッ トを効率良く使うのに有効だと述べました。しかし残念ながら、 select() 自身がそれほど効率的なものではありません。 この関数の引数のうち四つは、この関数を呼び出すたびに毎回設定しな くてはなりません。また、そのうち三つに対しては、 select() に渡すソケットの数を N とすると、 select()呼び出しの後に毎回 N 回のループを回さなくて はいけません。

select()を使うべきなのは、互換性の理由があるとき くらいでしょう。Unix と Windows CE 上で、同期型でないI/O戦略を効 率良く行うには、非ブロック型ソケットに対して select() を使う方法しかありません。その他の場合には 全て、より良い代替手法が存在します。

経験則4: 非常に大量のデータを扱うプログラムでは、非同期型ソケットは使わない。

ウィンドウメッセージは、ソケット上で何かが起こったことを通知 するには最も遅い方法です(select()を除いてはね)。こ こで言いたいのは、ウィンドウメッセージのキューが非効率だと言って いるのではなく、以下で述べる他の手法ほどには効率的でない、という だけです。

経験則5: 高性能サーバでは、オーバーラップI/Oが望ましい。

さまざまなI/O戦略の中では、オーバーラップI/Oが最も高性能です (I/O完了ポートの方がさらに効率が良いのですが、Winsock 準拠という ものに比べると標準的ではないので、このFAQでは扱いません)。オーバー ラップI/Oをきちんと使うと(あと、サーバ上にメモリを山ほど積んで ね!)、一台のサーバでも何万 という数のコネクションをサポートすることができます。他のI/O 戦略では、このオーバーラップI/Oに匹敵するスケーラビリティを持つ ものはありません。

経験則6: 中規模の数のコネクションをサポートするためには、非 同期ソケットとイベントオブジェクトを検討する。

もしサーバが必要とするコネクション数が中規模(例えば100 から 1000 くらい)であれば、オーバーラップI/Oは必要ないかもしれません。 オーバーラップI/Oのプログラムを書くのは簡単ではないので、そこま での効率性が必要なければ、より簡単なI/O戦略を採用して余計なトラ ブルから身を守るのも良いでしょう。

非同期ソケットは、プログラムを正しく書けば、中規模のコネクショ ン数をサポートする専用サーバ用としては十分妥当な選択肢です。この 手法の主な問題点は、多くのサーバではユーザインタフェースを持って いない、すなわちメッセージループを持っていない、ということです。 UIを持たないサーバで非同期ソケットを使うには、非同期ソケットをサ ポートするための単独の非表示ウィンドウを作成しなければなりません。 ですが、もしユーザインタフェースを持っているプログラムであれば、 非同期ソケットはネットワークサーバの機能を追加するための方法とし ては最も楽な方法でしょう。

中規模の数のコネクションを扱うために妥当な選択肢としては、他 にイベントオブジェクトがあります。イベントオブジェクトはそれ本来 が非常に効率の良いものです。これによってぶつかる主な問題としては、 同時には 64個より多くのイベントオブジェクトを待ってブロックする ことはできない、ということです。それ以上でブロックするには、複数 のスレッドを生成して、それぞれがイベントオブジェクトのサブセット をブロックするようにする必要があります。この方法を選択する前に、 1024個のソケットを扱うためにはスレッドが16個必要となる、というこ とを考慮に入れてください。アクティブなスレッドの数がシステム上の プロセッサの数よりも多くなった場合には、深刻な性能問題を引き起こ しはじめます。従って、1024個のソケットというのが実質的な最上限値 です。

一つだけ警告: 一般公開されているインターネットサーバで受け付 ける同時接続数は、非常に過小評価してしまいがちです。今の見積もり では同時クライアント数が数千に満たない場合でも、巨大なスケーラビ リティを持つような設計をする価値があるかもしれません。まあ一方で は、今日の便利な使い捨てプログラムの方が、来月の素晴らしいプログ ラムよりも常に勝っている、ということがはっきりし始めているんです けどね。

経験則7: トラフィックの少ないサーバでは、ほとんどどのI/O戦略でも使える

トラフィックの少ないサーバでは、非常に高性能である必要がある 呼び出しはあまりありません。サーバによっては単に非常に多数のコネ クションのサポートだけが不要の場合もあるし、もし Win9x 上で開発 しているのであれば、その時点で既に同時には最大 100 ソケットに制限されて しまっています。接続数が1〜100程度の場合に適した戦略には、イベン トオブジェクト、select()を使った非ブロック型ソケッ ト、非同期ソケット、ブロック型ソケットを用いたスレッド、がありま す。

始めの三つについては既に説明してありますので、ブロック型ソケッ トを用いたスレッドについて考えてみましょう。この方法は、サーバを 書くときには最も簡単な方法であることが多いです。メインループでコ ネクションを受け付けて、新規のコネクションを受け付けたらそれ専用 のスレッドを起こして、その中でブロック型ソケットを取り扱う、とい うだけです。ブロック型ソケットにはいくつかの利点があります。まず、 効率が良いです。なぜなら、スレッドがブロックすると、オペレーティ ングシステムが即座に別のスレッドを実行させるからです。また、同期 型のプログラムは、同等の処理を行う非同期型のプログラムに比べると、 より率直で簡単です。

この方式には主に二つの問題があります。第一に、スレッドは非常 に多くの同期処理を必要とすることが多いです。これを間違いなく行う のは大変で、ブロック型ソケットを使う利点を台無しにしてしまうかも しれません。第二に、スレッドはまともにスケールしません。イベント オブジェクトでの議論を思い出して欲しいのですが、アクティブなスレッ ドの数がシステム中のプロセッサ数をはるかに越えるような場合、効率 上の問題が発生するのです。従ってこの方式は、接続数が非常に少ない 場合か、中程度の接続数でほとんどが待ち状態であるような場合にしか 使えません。

経験則8: ユーザインターフェースのスレッド内ではブロックしないこと

この経験則は、 Windows プログラミングでのルールそのままのよう にも聞こえますが、ほとんどのプログラムは単一スレッドであるために あえてここで持ち出しました。単一スレッドのGUIプログラムでは、UI スレッドをブロックさせるWinsock関数を呼び出してしまうと、ボタン は押せなくなるわ、メニューは開かなくなるわ、スクロールバーは動か なくなるわ、キーを押しても反応しないわ…、UIがフリーズしてしまう のです。

経験則9: GUIクライアントプログラムには、非同期ソケットが望ましい

この経験則の理由には二つあります。

  1. 非同期ソケットは、最初からGUIプログラムとうまく動作する ように設計されています。仮に、ウィンドウループが動いていてウィ ンドウ制御を行うコードも既に動いているプログラムがあったとし ましょう。これに非同期ネットワーク入出力を追加するのは、ダイ アログを一個追加するのと同じくらい簡単なのです。
  2. 他の方法はどれも、前項の経験則を満たすために、ネットワー ク制御用のスレッドを少なくとも一つ追加する必要がありました。 非同期ソケットでは、ネットワークとUIの両方を一つのスレッドで 扱うことができます。ウィンドウメッセージは到着順に一つずつ処 理されるので、同期も自動的に取られます。

経験則10: クライアントプログラムではスレッドはめったに役に立たない

初めてスレッドを学んだとき、プログラマはとにかく自分のプログ ラムでスレッドを使ってみたいと思うことでしょう。彼はスレッドにい くつかの利点があることを理解していますが、しかし逆に欠点があるこ とを知りません。不幸なことに覚えたて新米プログラマにとっては、そ れらの欠点が非常に重大な結果をもたらしてしまうのです。

スレッドの本来の利点の一つとして、ブロック型ソケットで入出力 を行うスレッドはまっすぐな制御の流れを持つため、理解しやすいとい う点があります。非同期型のプログラムは分散しているため、プログ ラミングもデバッグもより難しくなります。

他にスレッドの目立った利点としては、カプセル化っぽいことがで きるという点です。つまりプログラムをいくつかのスレッドに分割して、 それぞれにひとまとまりの仕事を与えられるわけです。しかしこれは、 各スレッドの処理が、プログラムの他の部分からはほぼ独立している場 合にのみ言えることです。そうでなければ、共通のデータ構造などを通 して各スレッド間でデータを共有しなければならなくなります。これで は本来のカプセル化が果たせません。

最後に、スレッドに関する最大の問題点は、これも共通データ構造 に関係するもの、すなわち同期です。この問題についての説明はもっと 他に良い場所があるでしょうから、ここでは多くは述べません。一言で 言うと、同期を正しく行うのは難しい、ということです。ちゃんと同期 が取れていないスレッドでは、処理の直列化による遅延、コンテキスト 切り替えによるオーバヘッド、デッドロック、競合の発生、データの破 壊、といったトラブルに見舞われます。これらの問題は難しい問題であ り、ほとんどのプログラムにおいては、この問題を克服するだけの価値 があるほどスレッドの利点は大きくないのです。

もっとまともな代案としては、非同期型I/Oを使う、という方法があ ります。これにより前項の経験則で述べた同期に関する利点が得られま す。また各ソケットごとに不可視ウィンドウを作成することによって、 スレッドとほぼ同様な方法でアプリケーションを分割することもできる のです。もし二つの別個のソケットがあるとすると、それぞれのソケッ トに対する通知はそれぞれ別のウィンドウに送られるのです。これを API 用語でそのまま言い換えると、それぞれのソケット毎に別々の WndProc() を持たせる、ということになります。また、 MFCのフレームワーク用語で言い換えると、それぞれのソケット毎のコー ドをそれぞれ別の CWnd のサブクラスに記述する、とい うことになります。

経験則11: スレッドは、プログラムの他の部分への影響が全て丸く 収まる場合にのみ使うべし

前項の経験則では、スレッドは正しくプログラムするのは非常に困 難であるということを警告しました。しかし、スレッドは非常に便利な 場合もある、ということも事実です。ちょっとだけ設計を工夫すること で、スレッドを使うことによってプログラムが改善されるか否かをうま く推し量ることができます。各スレッドと、プログラムの他の部分の間 には、きれいなインタフェースが存在しているでしょうか? 存在してい るのであれば、同期の問題は簡単になります。もし存在していなければ、 予測不能なクラッシュとデータの破壊という混乱をもたらすだけでしょ う。

スレッドが活用できる場面には、例えば以下のようなものがありま す。

  1. FTPサーバ: FTP サーバを作る方法の一つとして、入っ てくるネットワークコネクションをメインスレッドで受け付けて、 それぞれのコネクションを別々のスレッドに投げる、という方法が あります。そして各スレッドでは、入ってきたFTPコマンドを処理 して必要な応答を送信し、セッションが閉じられるとスレッドは終 了します。各スレッドは他のスレッドとは関わり合いを持つ必要は 無く、また全てのスレッドは全く同じように動作するので、まさに これはスレッドの理想のアプリケーションの一つなのです(ただし、 上記で述べたサーバ関連の経験則は心に留めておいてください。つ まり、一クライアント毎に一スレッドを作るのは、サーバのスケー ラビリティにおいて非常に厳しい制限がついてしまいます)。
  2. ウェブブラウザ: 最近のウェブブラウザでは、ファイ ルをダウンロードしているときでもブラウジングを続けられるよう に、ファイルをバックグラウンドでダウンロードするようになって います。このダウンロードストリームはまず間違いなく、専用のス レッドで処理されているでしょう。
  3. 電子メールプログラム: 電子メールプログラムは、そ の第一目的は普通、電子メールを読むことと書くことです。しかし、 電子メールのメッセージが送信されるときは、ユーザの作業を中断 しないのがベストです。こういう場合別のネットワークスレッドで メッセージの送信を行うことで、プログラムの他の部分への影響を 最小限に押さえることができます。
  4. 株価変動表示: 株価変動表示の原理は、小さな量の連 続したリアルタイムデータを、愛嬌のある見やすいフォーマットで 表示するというだけのものです。関連するネットワークデータの量 が少なければ、スレッド同期のオーバーヘッドは無視できる程度の ものでしょう。加えて、この種のアプリケーションでは、保護の必 要なデータ構造は一個しかありません。同期が本当に大きな問題と なってくるのは、複数のデータ構造を保護する必要が出てきたとき なのです。

経験則12: プロトコル周りの設計

ネットワークプロトコルには、本質的に同期型のものもあれば、そ うでないものもあります。同期型プロトコルの例としては、電子メール の POP3 プロトコルがあります。これは、ユーザ名を送信し、応答を受 け取り、パスワードを送信し、応答を受け取り、メールのリストを取得 するための要求を送信し、応答を受け取り…と続きます。POP において は、これらのコマンドは特定の順序で送信しなければなりません。例え ばパスワードはユーザ名の前に送信することはできませんし、ユーザ名 とパスワードを送信せずにメールのリストを取得することはできません。 POPクライアントを非同期型ソケットを使って書こうとすると、状態遷 移マシンを書く必要が出てきます。

一方、もし非同期型のプロトコルであれば、非同期型ソケットを使 うのも良いでしょう。非同期型プロトコルは、関数呼び出しの集まりに 似ていることが多いです。例として、ネットワークにつながったSQL デー タベースからデータを受信するプログラムを考えてみましょう。つまり SQL文を送信し、その結果の集合を受信するというものです。この場合、 それぞれの「関数呼び出し」の後には、プログラムは初期状態に戻りま す。つまり、プロトコル中のどの状態にいるのか、ということを覚えて おく状態マシンを保持する必要がないのです。

経験則 13: ブロック型ソケットの方が簡単、非ブロック型ソケッ トの方が強力

この経験則は、ここまで上記で述べてきた内容の全てを言い換えた ようなものです。繰り返しになりますが、ブロック型ソケットは簡単で あるという点が魅力ですが、その欠点を考えたときに、結局は何らかの 非ブロック型ソケットを使うように再設計せざるを得ない、ということ があり得ます。特に、一個より多くのソケットをサポートするプログラ ムではそれがあてはまります(実質的に全てのサーバプログラムはこの カテゴリに分類されます)。ブロック型ソケットを同時に複数使うため に妥当な方法はただひとつ、スレッドを使用することだけですが、しか し非ブロック型ソケットを用いたほうが、より幅広い設計上の選択肢が あります。

まとめ

ここで述べた経験則があなたの手助けになれば幸いです。中には同 意できない経験則もあるかも知れませんが、少なくともご自分の選択を 見直して頂けたのではないかと思っています。設計は非常に主観にもと づく作業であり、この経験則リストは主に私の考えと好みに基づいてい るものです。

Philippe Jounin氏に感謝の言葉を捧げます。彼にはこの文書の 1998 年版においてコメントをいただきました。2000年版では、私の経 験が増えたことに加え、David Schwartz 氏と Alun Jones 氏からのコ メントを反映しています。両氏には、 Winsock サーバを作るための正 しい方法について、私の考えをさらに推し進めていただきました。

Copyright © 1998-2001 by Warren Young. All rights reserved.


<< せっかちな人のためのWinsock TCP を有効に使うために >>
Last modified: $Id: io-strategies.html,v 1.6 2002/11/09 20:40:33 ksk Exp $ Go to the original FAQ page
< Go to the main FAQ page << Go to the Home Page