🐧

同期・非同期 / ブロッキング・ノンブロッキング ~ そろそろ完全に理解する ~

に公開

はじめに

同期・非同期、ブロッキング・ノンブロッキングについて「完全に理解した」と5000兆回くらい言い続けているので、そろそろ記事にまとめておく。これで終わりにするか、続けるか・・・。

基本概念

同期処理と非同期処理

同期処理と非同期処理はタスクの実行制御と結果の取得(完了通知)パターンに関する概念だ。

同期処理とは、タスクが順次実行され、前のタスクが完了するまで次のタスクが開始されない処理モデルを指す。料理で例えると、レシピの手順を一つ一つ順番に行い、各ステップが完了してから次に進むようなものだ。

一方、非同期処理はタスクの開始と完了が分離されているモデルを指す。タスクを開始した後、その完了を待たずに他のタスクを実行でき、元のタスクの結果は後で通知やコールバックなどの形で受け取る。これは複数の料理を同時に進行させるシェフのようなものだ。

観点 同期処理 非同期処理
実行制御 呼び出し元が処理完了まで制御を保持 呼び出し元は制御を手放し次の処理に進む
完了通知 呼び出し元が能動的に完了を確認 呼び出し元が受動的に通知を受け取る
コード実行順序 記述順序に従って確定的 イベント発生順序に依存し非確定的
プログラムフロー 直線的で追跡しやすい イベント駆動で複雑になりがち

ブロッキングとノンブロッキング

ブロッキングとノンブロッキングは、操作がリソース(特にI/O)にアクセスする際のプロセス(スレッド)の状態に関する概念だ。

ブロッキング操作とは、操作が完了するまで実行プロセス(スレッド)が待機状態になり、他の作業ができなくなる操作を指す。これは、エレベーターのボタンを押して到着まで待つ状況に似ている。

一方、ノンブロッキング操作は、リソースにアクセスする際に即座に制御を呼び出し元に返す操作のため、プロセス(スレッド)は停止しない。リソースがすぐに利用可能でない場合でも、エラーやステータスコードを返して即座に制御を戻す。これは、エレベーターのボタンを押してすぐに別の用事を済ませ、定期的にエレベーターの状況を確認する状況に似ている。

システムコールレベルでの動作の違いを以下に整理する。

観点 ブロッキング呼び出し ノンブロッキング呼び出し
待機動作 操作完了まで実行を停止 即座に制御を返す
リソース利用不可時 プロセス/スレッドが待機状態 エラーコード返却(EAGAIN等)
CPU使用率 待機中は0% ポーリング時にCPU消費

ボトルネックと概念の組み合わせ

システム開発において、I/O操作は処理時間の大部分を占めることが多い。CPUと比較してI/O操作は桁違いに遅く、効率的なI/O処理はシステムパフォーマンスに大きく影響する。

以下の図は、I/O操作とCPU処理の時間スケールの違いを示している。

https://colin-scott.github.io/personal_website/research/interactive_latency.html 参考(2020年時点)

この図からわかるように、I/O操作はCPU処理に比べて桁違いに遅い。この大きな時間差が、同期・非同期、ブロッキング・ノンブロッキングの各アプローチが重要になる理由だ。

同期・非同期とブロッキング・ノンブロッキングの概念を組み合わせると、以下の4つの異なるモデルが生まれる。それぞれのモデルは特徴的な動作原理と適用場面を持っている。

I/Oモデルの4パターン分析

パターン毎の早見表

4つのI/Oモデルの特性を比較した早見表を次に示す。

特性 同期ブロッキング 同期ノンブロッキング 非同期ブロッキング 非同期ノンブロッキング
I/O要求時の動作 完了まで停止 即座にリターン select/poll呼び出しで待機 即座にリターン
データ転送時の動作 アプリケーションが停止 アプリケーションが停止 アプリケーションが停止 カーネルがバックグラウンド実行
完了検知方法 関数リターン ポーリングによる状態確認 select/pollからのリターン ・シグナル
・コールバック
・イベント通知
代表的システムコール ・read()
・write()
・accept()
・read() (O_NONBLOCK)
・write() (O_NONBLOCK)
・select()
・poll()
・aio_read()
・aio_write()
・io_uring

同期ブロッキングI/O

最も直感的で実装が簡単なモデルだ。レストランで注文後、料理が出来上がるまでテーブルで待ち続ける状況に例えられる。

このモデルの問題点は、多数のクライアントを同時に扱う際にスレッド数が膨大になることだ。1万接続を処理するには1万スレッドが必要となり、メモリ消費とコンテキストスイッチのオーバーヘッドが深刻になる。

同期ノンブロッキングI/O

ファイルディスクリプタをノンブロッキングモードに設定したI/O操作だ。データが準備できていない場合、EAGAINやEWOULDBLOCKエラーを即座に返す。

このモデルは、エレベーターボタンを押して、定期的に到着をチェックしながら他の作業を続ける状況に似ている。ただし、チェック自体がCPU時間を消費するため、ポーリング間隔の調整が重要だ。

非同期ブロッキングI/O (I/O多重化)

selectやpollシステムコールを使用したモデルだ。複数のファイルディスクリプタを監視し、いずれかが準備完了になるまで待機する。

このモデルは、複数の料理を同時に調理しているシェフが、どの鍋が沸騰したかを監視している状況に例えられる。単一のスレッドで多数の接続を効率的に処理できるが、select自体がブロッキング呼び出しである点に注意が必要だ。

非同期ノンブロッキングI/O

POSIX AIOやLinuxのio_uringを使用したモデルだ。I/O操作全体(データの準備からアプリケーションバッファへのコピーまで)がカーネル内で非同期実行される。

このモデルは、宅配便の配達を依頼して、到着時にベルで通知してもらう状況に最も近い。依頼者は配達完了まで他の作業に集中でき、最も効率的なI/Oモデルといえる。

まとめ

完全に理解した。

Discussion