非同期I/O APIの分類学

nmoshに色々な非同期I/O APIバインディングを書いていると、非同期I/O APIはいくつかのパターンがあることがわかってきた。
基本的には、完了通知の方法 x リクエストの方法と言える。

リクエスト手法

リクエスト手法は以下の点で分類できる

  • リクエストに事前の"チケット"発行が必要なケース
    • チケットを再利用できないケース(存在しない?)
  • チケットを必要としないケース
  • 一度発行すると複数回完了するケース - readリクエスト等

例えば、Win32の非同期I/O(Overlapped I/O)はリクエストの発行にOVERLAPPED構造体が必要になる。一つのin-flightリクエスト毎に一つのOVERLAPPED構造体が必要になる。
逆に、一般的なPOSIXの非同期I/Oは、リクエストの発行にチケットを必要としない。
nmoshはリクエストの発行にチケットを必要としない方式を採用している。これはあまり良くない判断で、チケットに相当するオブジェクトを明示的にユーザに作らせないと、リクエストのキャンセルのために個々のリクエストを指定する良い方法が無くなってしまう。

ディスパッチ関数によるcallback呼び出し

nmosh(や、元ネタとなったlibuv)はこの方法が唯一の非同期I/O完了通知になる。ディスパッチ関数は、コールバック関数の実行コンテキストを決めるために用いられる。
ディスパッチ関数を呼び出すと、I/Oの完了状態がチェックされ、完了していれば通知用として指定されたcallbackをそのコンテキストで呼び出す。
ディスパッチ関数はさまざまな名前で呼ばれる:

  • pcap_loop, pcap_dispatch - pcap / WinPcap
  • libusb_handle_events - libusb
  • uv_run - libuv

ディスパッチ関数の呼び出しから抜けるコンディションはなかなか難しい。uv_runの場合は、監視対象のストリーム全てが終了したタイミングで抜ける。pcap_loopの場合は、pcap_break_loop関数が有り、これを呼び出すと抜けることが出来る。
libusbの場合、同期I/O APIが非同期I/O APIの上に実現されているため、他のlibusb APIの呼び出しによりlibusb_handle_eventsが呼ばれる可能性がある。なので、libusbの非同期I/Oは通常の人間にはかなりわかりにくい。このようなAPIが有るので、nmoshでは、I/O callbackはdispatch関数の呼び出したスレッド以外からよばれることを前提としていて、その中で(スレッドセーフな)chime手続き( http://d.hatena.ne.jp/mjt/20120916/p1 )のみを呼び出すこととしている。

Implicitなcallback呼び出し

Win32のコールバックを取るリクエストはこの方式で、特にディスパッチ関数を呼び出す必要は無い。Win32のI/OリクエストcallbackはAPC(Asynchronous Procedure Call)として抽象化されていて、Win32のスレッドはAPCのための優先度付きキューを持っている。
ただしAPCとしてキューされた継続を発動させるためには、threadを"Alertable状態"(待ち状態の一種)にしておく必要がある。普通のWait〜やSleepによってこのAlertableな待ち状態に入れる。

イベントオブジェクトへのシグナル

一般的な非同期I/Oはこちらで、Win32のIOCPやPOSIXのfdに対するシグナルという形でI/Oの完了(や、fdの場合は開始)を通知する。
このように通知手法には様々なバリエーションが存在するが、nmoshやlibuvは全てをコールバックに統一している。