基本的にはboost
のasync
なんかを使えば、OS固有のややこしい部分を隠蔽してくれる。あるいはQtのsignal/slot機構やQt Concurrentフレームワークを使うことで、完了処理を個別に定義して随時実行させてやることもできる。ただ、なんらかの事情でそういうのを使いたくないという場合、システムコールを直に使う必要がある(あるいは自分でカーネルを書くとか)。WindowsとUNIXで、似ているようで当然ながらけっこう違うので注意が必要。
ここでは、「読み・書きを、システムコールを使って必要な時に最短のCPU時間で行なう」ということをさしあたりの目的とする。つまり、
- ポーリングで無駄にCPUを使いたくない
- 入力があった時にはできるだけすぐに反応できるようにしたい(waitとかsleepとかをしたくない)
- OS固有の部分はあってもよいから、できるだけ環境が変わっても動くコードにしたい
というあたりを判断基準として、使えるシステムコールを考える。
UNIXのI/O
I/O多重化
UNIXの方法は同時にたくさんの読み・書き可能状態を監視できる、という意味から、非同期というより「多重化I/O」と呼ばれるらしい。
-
select(2)
を使う。基本的に全てのI/Oデバイスにはファイルデスクリプタが割り当てられるはずで、select(2)
でこれらからの割り込みを一気に監視してやることができる。- コアになるのは
fd_set
というビットマスク。FD_ZERO(&set)
マクロで全クリア、FD_SET(desc, &set)
で特定のマスクをセット、FD_CLR(desc, &set)
で特定のマスクをクリア。 - 読み込み可能状態、書き込み可能状態で別々の
fd_set
を用意してやる(書き込み可能は「書き込み完了」ではない;EAGAIN
やらEWOULDBLOCK
にならない、ということ)。読みあるいは書きでとくに待つ必要がないのであれば、そちらにはNULL
を指定することになる。exceptfds
なるものもあるけれど、これはOS依存の「例外的なイベント」を待つためのものらしい。 -
struct timeval
を用いてタイムアウトを設定できる(NULL
ならタイムアウトなし)。 -
fd_set
はselect(2)
の呼び出しごとに書き換えられるので注意。処理が戻ってきた時には、読み・書き可能状態かどうかを示すビットマスクになっている。FD_ISSET(desc, &set)
でチェックできる。
- コアになるのは
-
pselect(2)
は待っている間のシグナルマスクの設定もでき、タイムアウトがより細かいバージョン。 -
poll(2)
ではもう少し柔軟な処理ができるようになっている。struct pollfd
を使ってデスクリプタごとに監視すべきイベントのマスクを設定する。pollfd
の書き換えとか返り値とかは、select(2)
とほぼ同じ挙動。 - 並列的にいくつかの処理(読み込み+書き込み、とか)を行なうには、スレッドを分けるしかないのでは?このときには
pthread(3)
をさまざまに使うことになる。
ノンブロッキングI/O
ファイルデスクリプタをNONBLOCK
に設定することで、読み書きをブロックしないようにできる。この場合、書き込みバッファがいっぱいだったり読み込みバッファが空だったりするとEAGAIN
が返ってくる。そうすると、どこか適当なタイミングであらためて読み書きを行なう、ということになる。
非同期I/O
ここでいう「非同期」は、「完了した・しないに関わらず呼び出しから処理がすぐ返ってきて、I/O完了時に別のコールバックが呼び出される」というようなイメージ。
- 伝統的なUNIXシステムコールには非同期I/Oは存在しない。
-
AIO(4)
がlibc
で実装されている。aio_read(2)
やらaio_write(2)
やらがある。
Windowsの非同期I/O
Windowsでは、処理をオーバーラップさせられることから「オーバーラップドI/O」とも呼ばれる。この仕組みは、使い方によってノンブロッキングかつ非同期I/Oになる。
というかもともとは、「Windowsではselect
でソケット以外待てないのか!」というところがこの記事の動機になっている。
-
select
はソケット通信でしか使えないので注意。 - かわりに
ReadFile()
あるいはWriteFile()
をノンブロッキングにして、コールバック的なものを設定してやることができる。核となるのはOVERLAPPED
構造体(WinBase.h
中)で、大きく分けて以下の2つの使い方ができる:- 単一スレッドでひとつの処理を待つ場合は
GetOverlappedResult()
関数で待ち状態に入れる。読み書きが完了したら処理が戻ってくる。 - 単一あるいは複数のスレッドで複数の処理を待つ場合は、イベントオブジェクトを使える。
OVERLAPPED
構造体のhEvent
メンバーにイベントオブジェクトを設定すると、読み書きが完了したときにイベントがセットされるようになる。これらのイベントオブジェクトをひとところに集めて、WaitForMultipleObjects()
で待つことができる。
- 単一スレッドでひとつの処理を待つ場合は
- 上記のオーバーラップ機能を使いたい場合は、デバイスファイル自体を
FILE_FLAG_OVERLAPPED
フラグを立てて開いてやる必要がある。 - もっと柔軟に処理を並列化したい場合は、Windowsでも別のスレッドを立ち上げる必要が出てくる。
Windowsのルーチンの方が、どんな処理でもまとめてWaitForMultipleObjects()
で待てるので便利なのかもしれない。一方で、イベントオブジェクトをチェックする時に毎回排他処理がかかる気もするので、それがオーバーヘッドになってしまうのではという危惧もある。まあ最近のコンピュータなら、読み書きにかかる時間に比べて大したロスではないのかもしれないが。