ソフトウェア開発やシステム設計において、CPU 集約型 (CPU-bound) と I/O 集約型 (I/O-bound) のタスクを理解することは、アプリケーションの最適化や適切な技術スタックの選択において極めて重要です。これらの概念は主にアプリケーションの性能ボトルネックに関係し、開発者が効率的なマルチスレッドや非同期プログラムを設計する際に役立ちます。
まず、コンピューターの基本的な動作を単純に抽象化すると、以下のようになります:
入力(キーボード) -> 計算(CPU) -> 出力(ディスプレイ)
入力と出力は I/O に属し、計算は CPU に属する
次に、単体のプログラムは複数のメソッドや関数の直列・並列の組み合わせで構成され、以下のように表せます:
入力パラメータ -> 計算 -> 戻り値
最後に、分散サービスは複数の単体サービス(クラスタ)の直列・並列の組み合わせで構成され、サービス間のやり取りは次のように抽象化できます:
ネットワークリクエスト(入力パラメータ) -> 計算 -> ネットワークレスポンス(戻り値)
リクエストとレスポンスは I/O に属し、計算は CPU に属する
このように、ハードウェアとソフトウェアの両面から見ても、システムは I/O と CPU の計算によって構成されていることが分かります。
CPU 集約型タスク(CPU-bound)
CPU 集約型タスクとは、主に中央処理装置(CPU)の速度によって制限されるタスクを指します。これらのタスクは大量の計算を必要とし、処理時間の大部分が CPU に費やされ、外部リソース(ディスク I/O やネットワーク通信など)を待つ時間は少なくなります。
CPU 集約型タスクの特徴
- 高い計算要求:数学的に複雑な計算を伴うタスクが多く、例えばビデオのエンコード・デコード、画像処理、科学計算などがあります。
- マルチスレッドの有利性:マルチコア CPU を利用すれば並列処理により計算速度を大幅に向上させることができます。例えば、複数のスレッドにタスクを分割して各コアで処理させることで、効率が向上します。
- リソース消費:CPU 集約型タスクを実行すると CPU の使用率が 100% に近くなることが多い。
代表的な CPU 集約型タスクの例
- データ分析や大規模な数値計算
- グラフィックレンダリングやビデオ処理
- 暗号通貨のマイニング
ノートパソコンのファンがうるさくなるような状況では、ほとんどがこのタイプのタスクです。
CPU 集約型タスクの最適化方法
- 並列化:マルチコアプロセッサを活用して並列計算を行うことでパフォーマンスを向上させる。
- アルゴリズムの最適化:計算量を減らすために、より効率的なアルゴリズムを使用する。
- コンパイル最適化:高性能計算に適したコンパイラやコンパイル最適化手法を活用する。
I/O 集約型タスク(I/O-bound)
I/O 集約型タスクとは、主に入力/出力(I/O)の処理、すなわちディスク I/O やネットワーク通信の待ち時間によって制限されるタスクを指します。これらのタスクでは、CPU の処理時間は比較的短く、主なボトルネックは I/O 操作の完了を待つ時間にあります。
I/O 集約型タスクの特徴
- 高い I/O 需要:頻繁なファイルシステムの読み書き、大量のネットワークリクエストなどが特徴。
- 並行処理が有利:I/O 集約型タスクは、イベント駆動型や非同期プログラミングを活用することで効率的に処理できる(例:Node.js の非ブロッキング I/O)。
- 低い CPU 使用率:大部分の時間が外部 I/O 操作の待ち時間に費やされるため、CPU の使用率は通常低い。
代表的な I/O 集約型タスクの例
- ウェブサーバーやデータベースサーバー(大量のネットワークリクエストを処理)
- ファイルサーバー(頻繁なディスク読み書きを実行)
- クライアントアプリケーション(メールクライアントや SNS アプリなど、ネットワーク通信が頻繁に発生)
I/O 集約型タスクの最適化方法
- キャッシュの活用:メモリキャッシュを利用してディスク I/O の回数を減らす。
- 非同期プログラミング:非同期 I/O 操作を活用し、アプリケーションの応答性とスループットを向上させる。
- リソース管理の最適化:不要な I/O 操作を削減し、効率的にリソースを管理する。
Node.js の非ブロッキング I/O
Node.js は、非ブロッキング I/O モデルの代表的な実装であり、単一スレッドで大量のクライアントリクエストを効率的に処理できるようになっています。これは、イベントループを活用したアーキテクチャによるものです。
非ブロッキング I/O とは?
非ブロッキング I/O(Non-blocking I/O)とは、I/O 操作を実行する際に、プログラムが I/O の完了を待たずに次の処理を進められる方式のことです。これにより、プログラムは I/O 操作の待ち時間中も他のタスクを実行できます。
Node.js の非ブロッキング I/O の仕組み
Node.js は JavaScript を V8 エンジン上で実行し、libuv
ライブラリを利用して非ブロッキング I/O と非同期プログラミングを実現しています。主な要素は以下のとおりです:
- イベントループ:Node.js の非ブロッキング I/O の中核。ネットワーク通信やファイル I/O などの非ブロッキング処理を実行しつつ、UI イベントやタイマー処理も同時に行う。
- コールスタック:同期タスク(計算や即時実行可能な処理)はコールスタックで実行される。長時間処理するとメインスレッドがブロックされる。
- コールバックキュー:非同期処理の完了後、対応するコールバック関数がキューに追加され、イベントループによって処理される。
非ブロッキング I/O の例
const fs = require('fs');
fs.readFile('./test.md', 'utf8', (err, data) => {
if (err) {
console.error('ファイル読み込みエラー:', err);
return;
}
console.log('ファイル内容:', data);
});
console.log('次の処理');
在この例では、fs.readFile
は非同期で実行されます。Node.js はファイルの読み取りが完了するのを待たずに console.log('次の処理')
を実行します。ファイルの読み取りが完了すると、コールバック関数がコールバックキューに追加され、最終的に実行されてファイルの内容が表示されます。
イベント駆動とコールバックを活用することで、単一スレッドでも複数の操作を同時に処理できるため、多数の I/O 集約型リクエストを処理する際にアプリケーションの性能とリソース利用率を大幅に向上させることができます。
非ブロッキングのファイルシステム操作
Node.js がファイルシステムの操作(例えばファイルの読み取り)を実行する際、直接 POSIX ファイルシステム API を使用するのではなく、libuv
を利用します。libuv
は最適な方法を選択し、イベントループをブロックしないように処理を実行します。
libuv
は、オペレーティングシステムレベルのブロッキング I/O 呼び出しを処理するために、固定サイズ(デフォルトで 4 つ)のスレッドプールを維持しています。つまり、ファイル I/O 操作はこれらのスレッド上で非同期に実行されるため、メインのイベントループスレッドがブロックされることはありません。
Node.js のファイル操作は、同期呼び出しと非同期呼び出しの両方をサポートしています。しかし、libuv
の公式サイトによると、プラットフォームを問わず利用できる完全な非同期ファイル I/O API は存在しません。そのため、Node.js の非同期ファイル I/O は、スレッドプール内で同期ファイル I/O を実行することで実現されています。この仕組みは、いわゆる 生産者-消費者モデル に基づいています。
libuv
のスレッドモデル
libuv
のスレッドは、大きく 2 つの部分に分かれています。一つは メインスレッド、もう一つは スレッドプール です。メインスレッドの役割の一つは、タスクを記述し、それをスレッドプールに渡すことです。そして、スレッドプールがそのタスクを処理します。
例えば、非同期のファイル操作の場合、メインスレッドはファイル操作を記述したオブジェクトを生成し、それを タスクキュー に追加します。スレッドプールの各スレッドは、このタスクキューからタスクを取得し、処理を行います。ここで、メインスレッドは生産者(プロデューサー)、スレッドプールの各スレッドは消費者(コンシューマー) という関係になります。そして、タスクキューがプロデューサーとコンシューマーをつなぐ役割を果たします。
libuv
は、通常の生産者-消費者モデルにもう一つのステップを追加しています。それは、スレッドプールでタスクの処理が完了すると、その結果をメインスレッドに渡し、メインスレッドが必要であればコールバック関数を実行する というステップです。そのため、libuv
のスレッドモデルは以下のようになります:
この生産者-消費者モデルの流れを整理すると、コードの処理は以下の 4 つのステップに分けられます:
- タスクキューの作成
- メインスレッドがタスクをタスクキューに追加(タスクの投入)
- スレッドプールがタスクキューからタスクを取得し実行(タスクの消費)
- スレッドプールがタスクの処理結果をメインスレッドに渡し、メインスレッドがコールバックを実行(コールバック処理)
スレッドプールのスレッドがタスク(例えばファイルの読み取り)を完了すると、そのタスクのコールバック関数をイベントループの待機キューに追加します。イベントループがこのコールバック関数を処理する順番になったとき、メインスレッドはそれを実行し、非同期タスクの一連の処理が完了します。これにより、重量級の I/O 操作を実行している間でも、メインスレッドは軽量なまま維持され、ユーザーインタラクションへの応答性を損なうことがありません。
まとめ
適切な処理方法と技術スタックを選択することは、アプリケーションのパフォーマンスを向上させる上で非常に重要です。
-
Node.js は I/O 集約型の Web アプリケーションに最適
→ 非ブロッキング I/O モデルにより、大量の同時ネットワークリクエストを効率よく処理できるため、スレッドリソースの浪費を防げる。 -
CPU 集約型タスクには、マルチスレッド対応の言語やプラットフォームが適している
→ Java、C++、Go などのマルチスレッド対応言語を使用すれば、マルチコア CPU の計算能力を最大限に活用できる。
システムのボトルネックを正しく理解し、それに応じた最適な設計を行うことが、アプリケーションのパフォーマンスを向上させる鍵となります。
私たちはLeapcell、Node.jsプロジェクトのホスティングの最適解です。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:
複数言語サポート
- Node.js、Python、Go、Rustで開発できます。
無制限のプロジェクトデプロイ
- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。
比類のないコスト効率
- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。
洗練された開発者体験
- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。
簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。
Xでフォローする:@LeapcellHQ