Rust: Non-blocking I/O programming with mio::Poll
概要
Linux カーネルのシステムコール epoll や FreeBSD (Mac OS) の kqueue、またはより古典的な POSIX 準拠の select や poll システムコールは大量のクライアント接続を効率的に処理するノンブロッキング I/O プログラミングに必要な機能である。これらの類似機能は複数のソケット (ファイル記述子) をまとめて監視し、読み込み / 書き込み可能になったソケットに対して通知を受けることができる。つまり、1 つのスレッドで複数の接続に対する入出力処理に対応できることから、多くのクライアントと接続して通信を行うサーバサイドに置いて C10K 問題を解決するためにイベント駆動型フレームワークの低レベル層で使用されている。
この記事では Rust の mio 0.7 の Poll
を使用した実装について説明する。なお Poll
ライクなプログラミングモデルとしては Java 標準の Selector
の説明も参考になる。
Table of Contents
- 概要
- edge trigger と level trigger
- 外部からポーリングへの介入
- EOF の判断
- Echo サーバ実装
edge trigger と level trigger
epoll や kqueue を使ったプログラミングでは 2 種類の動作に注意しなければならない。その一つは、通知対象の準備ができた時点で一回のみ通知を行う edge trigger という動作である。edge trigger の動作は特に readable イベントの時に注意する必要があり、イベント処理でデータを読み残してしまうと、次の通知が発生するまで残ったデータを読み込む機会を失ったり、それ以降のデータを読み込めなくなったりする可能性がある。したがって edge trigger に対処するプログラミングでは readable イベント処理で読み出し可能なデータを全て読み出さなければならない。
もう一つは読み出し可能なデータが存在する限り準備通知を行う level trigger という動作である。この動作では edge trigger とは逆に writable イベントに注意する必要がある。level trigger 環境において書込み可能なデータが存在しないのに writable 通知を有効にしておくと、無駄な処理のループによって CPU リソースを食いつぶすことになる。このため level trigger に対処するプログラミングでは、書込み可能なデータが無くなった時に writable 通知を無効化し、新しい書き込みデータが発生した時に有効化するといった切り替えを行う必要がある。
mio::Poll
を使って edge trigger と level trigger のランタイム環境の互換性を考慮したプログラミングを行うには、readable 通知に対して WouldBlock が発生するまですべてのデータを読み込みを行い、書き込み可能なデータが存在するかどうかで writable 通知の切り替えを行う必要がある。
外部からポーリングへの介入
mio::Poll
はスレッドセーフではない (mio に限らず Java の Selector
でのキーセット操作もそうだが) ため、一般に poll を使用したプログラミングモデルは 1 つのスレッド内で準備通知のポーリングとイベント処理を行う。しかし、スレッドの外部から新しいソケットを登録したり、readable / writable を変更したり、またポーリングのイベントループを終了するようなケースでは、外部からポーリングの中断を行う必要がある。
このようなケースでは、外部スレッドから mio::Waker
に対して wake()
を呼び出すことで Waker の構築時に指定した mio::Poll
の (多分別スレッドでの処理をブロックしている) ポーリングを中断することができる。また Waker のみでは単にポーリングを中断するだけであるため、中断後の処理にデータを渡すために channel (Sender / Receiver) を併用する必要があるだろう。
EOF の判断
poll を使ったノンブロッキングの読み込みでは、相手側からのクローズによる EOF は Read::read()
が 0 を返すことで検出することができる (ただし読み込みバッファサイズが 0 でなければ; API リファレンス参照)。読み込み可能だが単にデータが到着していないだけであれば read()
は WouldBlock が発生するため EOF と状況を区別することができる。
読み出し側で EOF を検出した場合、そのソケットはすでにピアによって正しい手続きでクローズされている可能性が高く、したがって以後ソケットが writable になることを期待することができず、送信バッファに残っているデータは破棄してソケットを poll の登録から外す必要がある。
mio には is_read_closed()
が用意されているが、動作が保証されている準備通知は is_readable()
と is_writable()
のみであることからヒントとしての使用に限定される。
Echo サーバ実装
以下は mio::Poll
を使用した Echo サーバの実装サンプルである。サーバソケット (TcpListener) と接続したピアのソケット (TcpStream) を同じ Poll で処理処理している。