flat7th

+ 通信処理方式の選択肢

created 2013-09-03 modified 2014-11-13 

このページの話題は、select とか、WSAWaitForMultipleEvents とか、libeventとか、C10Kとか、そういう話。

ソケット通信のプログラムの書き方は、大体下記の方式のどれかに落ち着きます。

通信処理方式 選択肢


(1)Unixシングルスレッドでselect
(2)Windowsでselect
(3)select方式の、OS共通化
(4)マルチプロセス
(5)マルチスレッド
(6)非同期
(?)iOSは...?




(1)Unixシングルスレッドでselect


  • ソケットはブロッキングでソケットAPIは同期。ただしブロックしないソケットを選択(select/poll/epoll)してからAPIをコールすることで、ブロックを防ぐ。
  • その上に、イベントコールバックを登録するAPIを載せると、それがlibeventやlibevやZebOSやXツールキットの方法。
  • Unix系OSで組むならこれが最高。C10K問題の本命。
  • メリット:速い。大量通信可能。シングルスレッドのため、共有リソースに対し排他が不要。デバッグしやすい。割り込まれるポイントが分かりやすい。
  • デメリット:一人が止まるとみんなが止まるため、長時間処理は複数イベントに分割する必要がある。
  • デメリットの対策として、適切な分割でマルチプロセスとすることが多い。

(2)Windowsでselect


  • Windows(Winsock2)ではselectで待てるソケットが、1 Select に対し64ソケット、という制限がある。(最新状況未確認)
  • その対処のため、
    • (2-1)1プロセス64ソケット未満でマルチプロセス。
    • (2-2)マルチスレッドを使う。(ただしソケット数分ではなく、もっと少ない)
  • ソケットはブロッキングでソケットAPIは同期で理論的にはOKのはずだが、Winsockのおかしな仕様のためソケットをノンブロッキングにする。
  • デメリット:
    • (2-1)ではC10Kに対応するにはプロセスが多すぎる。
    • (2-2)ではイベント処理はマルチスレッドを想定せざるを得ない。つまり共有リソースには排他アクセスが必要。

(3)select方式の、OS共通化


  • (1)と(2)の、デメリットを両方引き継ぐ。
  • Javascript の nodejs ならこれ、のはず。
  • Java の java.nio はこの方式だが、読み書きのAPIがあまりに特異なので、結局Java独特のコードになる
  • =Unix、Windows、に対し、Javaというもう一つの通信APIプラットフォームができてしまっている。

(4)マルチプロセス


  • ソケットはブロッキングでソケットAPIは同期。
  • Unixでは、forkできてかつその他のプロセス情報を引き継げるのでマルチプロセスも選択肢としてあり。
  • Windowsでは fork がないのでマルチスレッドになり、(5)になる。
  • Javaでは、同一VM内でプロセスを分ける概念がないのでマルチスレッドになり、(5)になる。

(5)マルチスレッド


  • ソケットはブロッキングでソケットAPIは同期。すべての受信ソケットに対し、その数のスレッドを生成。
  • 書き込みは、基本的に全部、書き込み専用の一つのスレッドでやる。
  • selectは使わない

  • メリット:
    • いろんな環境でプログラミング可能。
    • selectで待てない入出力路にも対応可能。
  • デメリット:
    • 同じマシンで他の方式より絶望的に遅い。
    • プログラムが(意外にも)煩雑、分かりにくい。例えば、受信を待っているスレッドを外部から停止するための手段が必要。
    • デバッグ・再現・確認の手順が難易度高い。

  • スレッド機能はOSごとに個別。なので、Unixのpthread、Windowsスレッド、Java、Androidマルチタスクと、プラットフォームごとに別ソース。
  • Javaはスレッド処理の書き方はOS共通だが、先述(3)のとおり通信APIが独特なので別プラットフォームであるのと同じ。
  • 素のJava と Android は Java で同一?→ No。メインスレッドとワーカスレッドとのやりとり方式が Android 固有なので、ほとんど別方式のソースとなる。


ボスクラス
 ♦ ♦ |_ 部下からの読み込み完了報告を受けるメソッド //パラメータで何の受信イベントが起こったかを指定。
 | |                                        //部下のReadスレッドから呼ばれ、メインスレッドに実処理をポストする。
 | |_メインスレッドクラス(内部クラス)
 |    |_ タスク刈り取りメソッド  //実処理を刈り取り、イベントに応じ各タスクメソッドを呼ぶ。
 |    |_ タスク1メソッド
 |    | ...                 //部下のOpen, Close, Writeメソッドを用いてコーディング
 |    |_ タスクNメソッド
 |
 |__○○入出力路クラス     //たとえば、ソケット
 |   ♦ |_ Openメソッド
 |   | |_ Closeメソッド
 |   | |_ Writeメソッド
 |   |_Readスレッドクラス(内部クラス)
 |       |_ Readスレッド関数 //の中で、Readを同期実行、パケット分割されたデータを
 |                           //アグリゲーション(集成)して、意味のある単位で読めたらボスに報告
 |
 |__□□入出力路クラス     //たとえば、Windowsコンソール、Android USB機器
     ♦ |_ Openメソッド
     | |_ Closeメソッド
     | |_ Writeメソッド
     |_Readスレッドクラス(内部クラス)
         |_ Readスレッド関数 //の中で、Readを同期実行、読めたらボスに報告

持ち主クラス
 ♦
 |_持たれるクラス

  • メモ:
    • Windows とか 特殊環境 とかだと、こんな書き方が必要になる、という話。
    • Unix系OSだとこんな書き方はしない。ほとんどの入出力ブロックデバイスはselectで待てるので、たとえばXのマウスイベントなんかもselectで待つ方式で書ける。→それが(1)。


(6)非同期


  • Unix aio。
  • Windows ASyncXxx。
  • Unix では(1)が高性能で分かりやすいため、非同期はほとんど不要。
  • Windows では(2)が腐っているため、時々ある。

  • (1)は、
    • 今ならこのソケットに 書き込んでも/読み込んでも ブロックしない、という状態でコールバックが起動され、
    • その中でバッファ操作と送信/受信操作をするのに対し、

  • 非同期方式では、
    • バッファに書き込んで送信APIに/バッファを空けて受信APIに リクエストした後、
    • バッファから送信した/バッファに受信した 後にコールバックを起動される点が、異なる。

  • 非同期処理を呼んだ後はそのバッファの該当位置はユーザ処理では操作禁止のため、
  • バッファの扱いに注意を要する。意外とプログラミング難易度高い。

(?)iOSは...?





その他


  • ブロッキング/ノンブロッキング = ソケットに対する修飾語。「ユーザプログラムの実行制御がなめらかに流れていくのを妨げるような(ブロックするような)」の意味。受信すべきデータがまだ届いていないソケットに対しrecvを呼んだとき、ブロッキングソケットだと、ユーザプログラムはそのまま待たされる。ノンブロッキングソケットだと、エラーが帰る。待たされない。ブロッキングソケットに対して、recvを呼んでも待たされない(=受信すべきデータが既に届いている)ソケットがどれかを調べるのが、select/poll/epoll。

  • 同期/非同期 = APIの内部処理方式に対する修飾語。ユーザプログラムに制御が戻った時点で、カーネル内でもう何も動いていないのが同期。ユーザプログラムに制御が戻った後、カーネル内で何かが動いているのが非同期。非同期の場合、完了時点でユーザプログラムに通知するためのコールバック処理を、パラメータで渡すことが多い。

  • 非同期の説明で「カーネル」は「ミドルウェア」や「基盤API」に置き換え可能。

方式に共通的な注意:吐き出す処理を優先するよう意識的にプログラミングを行う必要がある。



低性能向けの共通APIって実装できないの?


(5)マルチスレッド 方式でできるんじゃないでしょうか。



高性能向けの共通APIって実装できないの?


できます。そこそこ難しいけど。

私は、上記の、Unixの(1)とWindowsの(6)を共通化したライブラリを作りました。
つまり、
  • APIがUnix/Windows共通で、
  • 完了時コールバック処理をパラメータに含む send/recv APIを呼ぶと、即座に返り、完了したらコールバック処理を起動してくれる
  • ユーザ処理は、事実上シングルスレッドと考えて、コールバック処理間では排他をせずに共有リソースにアクセスしてOK

というもの。


内部実装を簡潔に説明すると、

ユーザ向けAPIとして
  • コールバック登録型通信API と
  • リングバッファ風のバッファクラス を作り、
その内部実装では
  • スカタードバッファ型のソケット通信API({ポインタと長さ}の配列を渡すタイプのsend/recv)を使用する。配列の長さは高々2。
  • Unixでは、ブロッキングソケットと同期なソケットAPIを使用
  • Windowsでは、ノンブロッキングソケットと非同期なソケットAPIを使用
  • Unix libevent(等)では、selectが終わった時点でコールバックを呼ぶ方式ですが、libevent(に該当する処理)には、ミドルウェア内で定義した「内部のコールバック」を指定します。その内部コールバックの中で、ソケットAPIが完了した時点で「ユーザが登録した完了コールバック」を起動します。これで、WindowsとAPIの振る舞いが共通化できます。

ということで、高負荷環境を想定した、高速で、プラットフォーム共通な、通信APIが実装できるのです。

が、塩漬けになって死んでいます。

こういうのは、どうせ出しても、「良い応用」がないと誰にも見つけてもらえずにそのままになる、ので...。

話を聞いてくださる/応用してくださる 方がおられたら、こちらから伺って勢い込んでご説明しますよ。シャキーン!
ご連絡は、トップページに書いてあるメールアドレスまでどうぞ!

…とか書いても、誰にもつながらないんだよな…トホホ




いやしかし改めて、プログラミングに関しては、Windowsは無用に敷居が高い。意味なく複雑だし、不統一だし。