{,非}同期{ノン,}ブロッキングIO

いまさらになってRustのfuturesに入門しようと思ったのだけれど、その前にそもそもの{,非}同期{ノン,}ブロッキングIOのことを全く分かっていなかったので、メモ。

「同期」「非同期」の定義

この界隈で一番面倒くさいのが、この言葉の定義じゃないでしょうか。割と人によって使い方が違う上に、そもそも(同期, 非同期)と(ブロッキング, ノンブロッキング)は似たような概念なので混用も目立ちます。そこで、まずは(同期, 非同期)の定義をはっきりさせておきましょう。

本稿では、wikipediaの定義を採用します。日本語と英語しか読めないので日本語版英語版を眺めてみました。

英語版

まずはより衆目にさらされているはずの英語版から。

In computer science, asynchronous I/O (also non-sequential I/O) is a form of input/output processing that permits other processing to continue before the transmission has finished.

これだけでは非同期とノンブロッキングの違いがよくわからないような気がするのですが、 permits other processing to continue before the transmission has finished に注目すれば、結局「データ転送(=どこかのメモリ空間からデータを管理しているメモリ上にコピーする、あるいはその逆の操作)を行うのが呼び出し側のスレッドか否か」というのが同期と非同期を分かつポイントになっているとわかります。

(注: データ転送について。ここではメモリマップドIOを仮定した書き方をしたため、一般のIOも基本的には「どこかのメモリ空間からデータを管理しているメモリ上にコピーする操作」と書いてよいです。パイプとかもあるので、この方が都合がよいです)

日本語版

この項目に関しては、日本語版の記述の方がはるかにクリアな形で同期・非同期の違いを書いています。

非同期IO (Asynchronous IO) とは、入出力の処理を、その要請元のプロセス・スレッドとは独立に(非同期に)行う、入出力のAPIの類型である。

むしろ、確かに英語版も同じことを言っているということがわかります。

定義と例

以上を踏まえてもう一度同期・非同期IOの定義を書いておきましょう。

『「わたし」(呼び出し元のスレッド)が「わたし」の管理する領域から/に対しカーネル領域に対し/からメモリコピーを行うような手続きを同期IO、「どこかの誰か」(別のスレッドに相当するもの)が「わたし」の管理する領域から/に対しメモリコピーを行うような手続きを非同期IOと呼ぶ』

これを踏まえれば、同期・非同期IOの簡単な例を挙げることができます。

(間違ってたら教えてください!)

「ブロッキング」「ノンブロッキング」の定義

同期・非同期の定義が済んだので、ブロッキング・ノンブロッキングを定義しましょう。直接的な記述はなかったのでここの定義はwikipediaの記事とその具体例から類推しました。

まず、代表的な「同期」「ブロッキング」処理は、上のlibcの例もそうですが、結局のところO_NONBLOCK渡さなかった場合のopen(2), read(2), write(2)によるIO処理がその例でしょう。この場合、例えばread(2)は処理が終わるまで返ってきません。「処理」というのは

  1. 対象となるメモリ領域を準備し、カーネルのメモリにデータをコピー
  2. カーネルのメモリ領域から「わたし」の管理するメモリ領域へ「わたし」がデータコピー

という2ステップからなります。

一方、英語・日本語版ともにwikipediaによれば、O_NONBLOCK渡した場合のopen(2), read(2), write(2)は、「同期」「ノンブロッキング」処理に分類されるそうです。どういうことでしょうか?そこで、まずO_NONBLOCKを使った典型的な処理を見てみましょう。コードをここに置いてみました。コードを見ればわかるのですが、O_NONBLOCKを渡した際のread(2)の動作は(若干不正確ですが)

  1. (カーネル中の)FIFOのメモリ領域にデータがないなら即座に返る。今回の例では、別プロセスがカーネルのメモリ領域にデータをコピーするまでは即座に返る
  2. あるなら、「わたし」がカーネルのメモリ領域からデータコピー

となっています。

二つを比べてみましょう。たしかに、どちらもカーネルからメモリをコピーするのは自分自身です。最初の定義に照らすことで、この二つの操作はどちらも「同期」操作であるということがわかります。一方、振る舞いの違いは、カーネルのメモリ領域にデータがない場合です。前者はコピー元のデータの準備(HDDからの転送だったり、FIFOへの書き込みだったり)が終わり、それからカーネルメモリにデータがコピーされるまで呼び出し元スレッドが待ちます。つまり、この「待ち」操作のことを「ブロッキング」と呼んでいるわけです。

定義と例

というわけで、ブロッキング・ノンブロッキングの定義をまとめてみましょう。

『カーネルメモリとその先のメモリ領域のデータ転送が終わるまで待つ手続きをブロッキングIO、待たない場合をノンブロッキングIOと呼ぶ』

さて、これを踏まえると、読み込みはブロッキングが自然、書き込みはノンブロッキングが自然に思えます。なぜなら読み込む場合は読み込みデータの準備ができていなければその先ができないわけですが、書き込みは(少なくともその後に他のIO操作がなければ)書き込んだ後いつ反映されよう大きな問題はないからです。

実際、Linuxカーネルにはページキャッシュという仕組みがあり、基本的には書き込みはいったんバッファにためられてから処理されます。libcのレベルでもバッファがあったことを思い出せば、確かに書き込み操作はそれらのバッファを超えない限り基本的にノンブロッキングであると言ってしまって問題ないのだと思います。

では一方、ノンブロッキングな読み込みはどうでしょうか。この場合、カーネルメモリにデータがロードされたか何度も問い合わせるか(ポーリング)、データロードが終わった時点で知らせてもらうかする必要があります。おそらく一番プリミティヴな例はDMAで、この場合はデータ転送が終わったら割り込みでカーネルに処理を戻しているようです(一般にそうかはわからないですが)。また、グリーンスレッドを採用する場合であれば、ポーリングとの相性もよさそうに思えます。

わかりにくい例え

ポエムです^^

同期ブロッキングIO

宅急便を自分で宅急便屋さんまで持っていき、荷物が届くまでずっと荷物を追跡している

同期ノンブロッキングIO

宅急便を自分で宅急便屋さんまで持っていき、気が向いたら荷物を追跡する。あるいは相手から荷物が届いた連絡が来るまでは宅急便のことを忘れる

非同期ブロッキングIO

宅急便屋さんに集荷依頼を出し、荷物が届くまでずっと荷物を追跡している

この例えからもわかりますが、非同期ブロッキングは意味不明です。epoll(7)に関しては後述します。

非同期ノンブロッキングIO

宅急便屋さんに集荷依頼を出し、気が向いたら荷物を追跡する。あるいは相手から荷物が届いた連絡が来るまでは宅急便のことを忘れる

{,非}同期{ノン,}ブロッキングIO API

最後に、Linuxにおける{,非}同期{ノン,}ブロッキングIOのAPIを書いて本稿を終わろうと思います。

同期ブロッキングIO

「データがそろうまで待ってデータを自分で取ってくる」or「データが送り先に届くまで待つ」

O_NONBLOCKを使用していないか、FIFOを扱っていない場合のread(2),write(2)。ただしページキャッシュは無視。

同期ノンブロッキングIO

「データがそろっているならデータを自分で取ってくる」or「データを送って届くかどうか確認しない」

FIFOに対しO_NONBLOCKを使用した場合のread(2),write(2)。

非同期ノンブロッキングIO

「データを届けてもらう」or「データを送ってもらう」

Linux AIO。ただしいろいろな理由によりあまりメジャーではないようです。

その他

いろいろ定義をしましたが、世の中というのは白黒に分けられないところがあり、微妙な例というのが存在しています。それがepoll(7)に代表されるIOの多重化APIです。

これらのAPIは、複数のファイルディスクリプタに対するread(2), write(2)の操作をサポートします。渡したファイルディスクリプタの準備は、もちろん「わたし」以外の誰かがやってくれます。一方、どのファイルディスクリプタの準備ができたかを知るためにはポーリングしなくてはいけません。この際、タイムアウトを指定して待つことができます。前者に注目するならばこれはノンブロッキングだし、後者ならばある意味ブロッキングといえます。

ファイルディスクリプタの準備ができた後は、自分でread(2), write(2)する必要があります。この点から、wikipediaの定義に沿えば、このAPIは同期的であるといえます。実際、英語版wikipediaでは、epoll(7)を同期的であるとしています。一方、なぜかこの文献では(文献中の定義とconsistentでないにも関わらず)poll系のAPIを非同期的であるとしています(もし理由がわかる方がいらしたら教えてください)。そのため、この文献に依拠した文献でも同様にepoll(7)を非同期的APIであると言っているものが見受けられます。

最後に

本稿では大体ずっと言葉の定義に終始しましたが、結局大事なのはそれぞれのAPIがどういう操作を行っているかというイメージです。この文章がその理解の一助になればいいと思います。

Comments

comments powered by Disqus