Carpe Diem

備忘録。https://github.com/jun06t

Non-Blocking I/O, I/O Multiplexing, Asynchronous I/Oの区別

概要

各言語がC10K問題をどう解決してきたかを調べてみたところ、Non-Blocking I/O, I/O Multiplexing, Asynchronous I/Oの区別がよく分からなかったので調べてみました。
正直なところ人によってちょこちょこ定義が異なるのではっきりとした答えはなさそうですが、自分で調べてしっくりした形をまとめます。

Blocking I/O

f:id:quoll00:20170628133720p:plain

ref: java Selector is asynchronous or non-blocking architecture - Stack Overflow

システムコール
↓
カーネルモードにコンテキストスイッチ
↓
処理が完了
↓
ユーザモードにコンテキストスイッチ
↓
ブロック状態から解放

という流れ。典型的な同期処理の動きです。

Non-Blocking I/O

f:id:quoll00:20170628174205p:plain

ref: java Selector is asynchronous or non-blocking architecture - Stack Overflow

ファイルディスクリプタにO_NONBLOCKをセットしてノンブロッキングモードに
↓
そのファイルディスクリプタを使ってシステムコール
↓
カーネルモードにコンテキストスイッチ
↓
すぐにEWOULDBLOCKを返す(ユーザモードにコンテキストスイッチ)
↓
ファイルディスクリプタの準備が完了するまで何度もpolling(そのたびにコンテキストスイッチが走る)
↓
ファイルディスクリプタの準備完了
↓
準備ができたデータグラムに対するシステムコール
↓
処理が完了
↓
ユーザモードにコンテキストスイッチ
↓
ブロック状態から解放

このようにすぐにユーザモードに戻るため、ブロックが起きません。
一方で何度もpollingして状態をチェックしなくてはいけません。
注意としてファイルディスクリプタの状態のチェックがノンブロッキングなだけであって、通常のファイルのI/O処理はブロッキングです。

I/O Multiplexing

f:id:quoll00:20170704223932p:plain

ref: java Selector is asynchronous or non-blocking architecture - Stack Overflow

システムコール
↓
カーネルモードにコンテキストスイッチ
↓
ファイルディスクリプタの準備ができたらユーザモードにコンテキストスイッチ
↓
準備ができたデータグラムに対するシステムコール
↓
処理が完了
↓
ユーザモードにコンテキストスイッチ
↓
ブロック状態から解放

Blocking処理を2回に分けたような形です。
しかし多重化という文字通り、前述のBlocking I/OやNon-Blocking I/Oと違うのは複数のファイルディスクリプタの状態を同時に監視できる点です。
この図ではファイルディスクリプタの準備が完了するまでブロックされているように見えるためメリットがなさそうですが、実際はこれを複数同時に行えるため、複数のシステムコールがあった場合はこちらの方が高速になります。

また大抵はこの時監視するファイルディスクリプタはノンブロッキングモードにするのが普通なので、Non-Blocking I/OとI/O Multiplexingはセットになっていることが多いです。

Asynchronous I/O

f:id:quoll00:20170628174222p:plain

ref: java Selector is asynchronous or non-blocking architecture - Stack Overflow

aio_readでシステムコール
↓
リクエストがキューされた時点ですぐ返る(ユーザモードにコンテキストスイッチ)
↓
他の処理を実行可能
↓
I/O処理が完了
↓
カーネルがI/O処理の完了を通知してくれる(callbackやsignal)

aio_readaio_writeというシステムコールを使うと、処理をブロックせず、かつI/O処理の完了を通知してくれるようになります。
なぜ非同期処理ができるのかというと、これは内部的に別スレッドを用意するからのようです。

The POSIX AIO is a user-level implementation that performs normal blocking I/O in multiple threads

ref: asynchronous - Difference between POSIX AIO and libaio on Linux? - Stack Overflow

その他調べる過程で疑問に思ったことまとめ

Asynchronous I/Oが一番良さそうだけど全部それを使わないのはなぜ?

  • 同期処理の方がロジックがシンプルになる
  • 別スレッドを使うのでメモリを消費する

といった理由だと思われます。


ならNon-Blocking I/O × I/O Multiplexingの方が良さそう?

  • ファイルディスクリプタの状態のチェックが高速なだけ
  • ファイルのI/O処理は結局ブロック処理

なので、ファイルディスクリプタの状態が重要であるソケットなど以外はメリットは小さいようです。


非同期で有名なNode.jsは何を使ってる?

Nodeのコアライブラリであるlibuvにその設計が載っています。

f:id:quoll00:20170628174314p:plain

ref: Design overview — libuv documentation

C10K問題をクリアするためにネットワーク周りはノンブロッキングI/O×I/O多重化になっています。こちらはシングルスレッドです。
しかしfsといったファイル周りは別スレッドを使ったAsynchronous I/Oで実現しています。こちらはシングルスレッドではないです。


golangはどうやってC10K問題をクリアしている?

goroutineというスレッドより遥かに軽量で動かせる仕組みを用意しており、それを使うことでブロッキングI/Oだけど処理できます。
※スレッドが最低2MBのスタックを必要するのに対し、goroutineは数KBで済む。

イベントループなしでのハイパフォーマンス – C10K問題へのGoの回答 | プログラミング | POSTD


goroutineはスレッドでありコルーチンって聞いた

goroutineのは複数のスレッドを持ち、各スレッドの中で複数のコルーチンが動いてます。
あるコルーチンがブロック処理で止まれば、残りのコルーチンを別スレッドへ移動させることで、全体として処理を並行で動かせます。
この動きを抽象化してユーザが意識しなくても使えるようにしています。


スレッドとコルーチンのスケジューリングの違い

スレッドはOSによってスケジュールされているため、スレッド自身がスケジューリングを気にする必要はないです。
一方、コルーチンはユーザースペースでのただのルーチンのため、自分自身でスケジューリングをしないといけないです。

ソース