基本的には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()で待てるので便利なのかもしれない。一方で、イベントオブジェクトをチェックする時に毎回排他処理がかかる気もするので、それがオーバーヘッドになってしまうのではという危惧もある。まあ最近のコンピュータなら、読み書きにかかる時間に比べて大したロスではないのかもしれないが。