並行処理と非同期処理
バックエンドが concurrency と asynchronous processing models を使って複数のタスクを効率的に処理する方法を学びましょう。
バックエンドシステムにおいて並行性が重要な理由
現代のバックエンドシステムは、多数のリクエストを同時に処理する必要があります。これを可能にするのが並行性です。
- サーバーは、複数のユーザーやシステムから継続的にリクエストを受け取ります。
- リクエストを1つずつ処理していると、深刻な遅延やボトルネックが発生します。
- 並行性により、各リクエストが完全に完了するのを待たずに、複数のリクエストを進めることができます。
詳細
実際のシステムでは、リクエストは1件ずつではなく、継続的かつ大量に到着します。バックエンドサービスは、毎秒数百件、あるいは数千件のリクエストを受け取ることがあり、それぞれに計算処理、データベースアクセス、外部API呼び出しが必要です。
サーバーがこれらのリクエストを厳密に順番通りに処理すると、新しいリクエストは前の処理が終わるまで待たなければなりません。その結果、キューが急速に増え、ユーザーにとって許容できないレイテンシーが発生します。
さらに問題を悪化させるのは、ネットワーク呼び出しやディスクアクセスのように待ち時間を伴う処理が多いことです。この待機中、CPUはアイドル状態になり、リソースを効率的に使えていません。
並行性は、処理を重ね合わせることでこの問題に対処します。あるリクエストがI/O待ちの間に、別のリクエストを処理できます。これによりシステムは稼働し続け、全体のスループットが向上します。
その結果、バックエンドシステムは応答性を維持しながら多くのユーザーを同時に処理できるようになります。これは現代のアプリケーションにとって基本的な要件です。
並行性と並列性
並行性は複数のタスクを進行中として管理すること、並列性は複数のタスクを同時に実行することです。
- 並行性では、タスクを切り替えながら進行させることができます。
- 並列性では、複数の CPU コアを使ってタスクを同時に実行します。
詳細
並行性は、システムが複数のタスクをどのように構成し、管理するかに焦点を当てます。サーバーは 1 つのリクエストの処理を開始し、I/O を待っている間にそれを一時停止し、別のリクエストに切り替えることがあります。タスクは順番に進行するため、限られたリソースでもシステムを効率的に保てます。
一方、並列性はハードウェアに依存します。マシンに複数の CPU コアがあれば、複数のタスクをまったく同時に実行できます。各コアは独立してそれぞれのタスクを実行し、全体の処理能力を高めます。
重要な違いは、並行性が調整とスケジューリングに関するものであるのに対し、並列性は同時実行に関するものだという点です。これらは異なる問題を解決しますが、しばしば一緒に使われます。
たとえば、バックエンドサーバーは非同期技術を使って数千件の同時リクエストを管理しつつ、複数の CPU コアに処理を分散して並列実行を実現することがあります。
この違いを理解することは、システムのパフォーマンス、スケーラビリティ、そしてさまざまなバックエンドフレームワークが負荷下でどのように動作するかを考えるうえで重要です。
スレッド
スレッドはプロセス内の実行単位であり、従来のバックエンドサーバーは複数のスレッドを使ってリクエストを同時に処理します。
各レーンはそれぞれ独自の実行経路を走ります。スレッドが増えるほど同時に処理できる仕事は増えますが、CPUまたはメモリが限界になるまでです。
- 各スレッドは、サーバー内で独立した実行の流れを表します。
- 複数のスレッドにより、サーバーは複数のリクエストを同時に処理できます。
- スレッドベースのモデルは、Java Spring Boot のようなバックエンドフレームワークで広く使われています。
詳細
プロセスはプログラムの実行中のインスタンスであり、そのプロセス内でスレッドは実際に作業を行う単位です。1つのプロセスには複数のスレッドを含めることができ、それぞれが同じメモリ空間を共有しながら独立して実行されます。
従来のバックエンドシステムでは、受信したリクエストはスレッドに割り当てられます。たとえば、リクエストが届くと、サーバーはスレッドプールからスレッドを割り当てて処理することがあります。そのスレッドは、ビジネスロジック、データベースクエリ、レスポンス生成を含め、リクエストを最初から最後まで処理します。
このモデルはシンプルで理解しやすいです。各リクエストに専用のスレッドがあるため、実行の流れは分離され、開発者は主に逐次的なスタイルでコードを書くことができます。
ただし、スレッドは無料ではありません。各スレッドはメモリを消費し、システムにオーバーヘッドを追加します。同時リクエスト数が増えると、スレッドを増やしすぎることで、コンテキストスイッチやリソース枯渇によってパフォーマンスが低下する可能性があります。
こうした制約があるため、現代のシステムではスレッドベースのアプローチと非同期技術を組み合わせることが多いですが、多くの本番システムはいまでもこのモデルに依存しているため、スレッドを理解することは依然として重要です。
ブロッキング操作とノンブロッキング操作
ブロッキング操作では、タスクが完了するまでスレッドが待機します。一方、ノンブロッキング操作では、スレッドは他の作業を続けることができます。
タスクが I/O を待つとき、ブロッキングモデルではワーカーが停止します。 ノンブロッキングシステムでは、待機中もレーンをアクティブに保ちます。
- ブロッキング操作は、結果が準備できるまで実行を一時停止します。
- ノンブロッキング操作では、システムが他のタスクの処理を続けられます。
詳細
ブロッキング操作では、スレッドがタスクを開始し、そのタスクが完全に完了するまで待ってから次に進みます。たとえば、スレッドがデータベースにクエリを送信した場合、データベースから応答が返るまで待機状態になることがあります。この間、スレッドは他の有用な作業を行えません。
これは、多数のリクエストを処理するシステムでは大きな制約になります。各スレッドが I/O の待機にかなりの時間を費やすと、スループットを維持するためにサーバーはより多くのスレッドを必要とし、リソース使用量とオーバーヘッドが増加します。
ノンブロッキング操作は異なるアプローチを取ります。待機する代わりに、スレッドはタスクを開始するとすぐに他の作業の実行を続けます。結果が準備できるとシステムに通知され、元のタスクを再開できます。
これにより、1つのスレッドで複数のタスクを効率的に管理できます。特に、待機時間が実行時間の大部分を占める I/O 集約型のワークロードで有効です。
ブロッキングとノンブロッキングの違いを理解することは重要です。なぜなら、それがバックエンドシステムのパフォーマンス、応答性、スケーラビリティの設計に直接影響するからです。
非同期 I/O
非同期 I/O により、サーバーはデータベースクエリやネットワーク呼び出しのような遅い処理を待っている間も、他のタスクの処理を続けられます。
- 多くのバックエンド処理では、データベースや API などの外部システムを待つ必要があります。
- 非同期 I/O は、こうした待機中にスレッドがアイドル状態になるのを防ぎます。
- このアプローチはスループットとリソース効率を向上させます。
詳細
バックエンドシステムでは、多くの処理は CPU-bound ではなく I/O-bound です。データベースの問い合わせ、外部 API の呼び出し、ディスクからの読み取りなどのタスクは、メモリ内計算よりも大幅に時間がかかることがあります。
これらの処理をブロッキング方式で扱うと、結果を待っている間スレッドはアイドル状態のままになり、システムリソースを無駄にし、サーバーが処理できるリクエスト数も制限されます。
非同期 I/O は、サーバーが処理を開始したあと、待機せずに別の作業へ進めるようにすることでこれを解決します。システムは保留中の処理を追跡し、結果が利用可能になった時点で処理を再開します。
これにより、1 つのスレッド、または少数のスレッドで多数の同時リクエストを効率よく処理できるため、非同期 I/O は高性能なバックエンドシステムにおける中核的な技術となっています。
イベントループモデル
イベントループモデルは、リクエストごとにスレッドを割り当てるのではなく、少数のスレッドでタスクを非同期に処理することで、多数のリクエストを扱います。
各リクエストがそれぞれのワーカーを占有します。
1つのループが多くのタスクを順番に処理します。
イベントループが重い処理をスレッドに振り分けます。
- 単一のイベントループが、キューからタスクを継続的に処理します。
- ノンブロッキング操作により、システムは待機を避けられます。
- 非同期操作が完了すると、コールバックが実行されます。
詳細
イベントループモデルでは、システムは各リクエストに個別のスレッドを割り当てません。代わりに、中央のループが新しいタスクを継続的に確認し、1つずつ実行します。リクエストが到着すると、イベントループはその処理を開始し、必要な I/O 操作をノンブロッキングで開始します。
これらの操作の完了を待つ代わりに、イベントループは他の受信リクエストの処理に移ります。これによりシステムは常に稼働し続け、スレッドベースのモデルでよくあるアイドル待機による時間の無駄を避けられます。
非同期操作が終了すると、その結果はコールバックキューに入れられます。イベントループは最終的にこのコールバックを取り出し、そのリクエストに必要な残りの処理を完了します。
このモデルのよく知られた例が Node.js です。Node.js は、単一スレッドのイベントループと async I/O を組み合わせて、コールバックと promises によって実行フローを管理しながら、数千の同時接続を効率的に処理します。
このアプローチは I/O が多いワークロードに非常に効率的ですが、1つの遅いタスクが他のすべてを遅らせる可能性があるため、イベントループをブロックしないよう慎重な設計が必要です。
競合状態
競合状態は、複数のスレッドが同じデータに同時にアクセスして変更するときに発生し、予測できない誤った結果を引き起こします。
- 競合状態は、複数のスレッドが適切な調整なしに共有データを操作するときに発生します。
- 最終結果は、実行のタイミングと順序に依存します。
- これは、並行システムにおけるバグの大きな原因です。
詳細
競合状態は、2つ以上のスレッドが同じデータを同時に読み取り、更新するときに発生します。これらの操作は同期されていないため、どのスレッドが先に実行されるかによって結果が変わり、システムの動作が予測できなくなります。
たとえば、2つのスレッドが同じ口座残高を読み取り、どちらも更新しようとすると、一方の更新がもう一方を上書きしてしまうことがあります。その結果、各操作自体は単独では正しく見えても、データは誤ったものになります。
このような問題は、常に発生するとは限らないため検出が難しいです。わずかなタイミングの違いで実行順序が変わり、再現やデバッグが困難なバグを引き起こします。
競合状態を防ぐには、ロック、アトミック操作、データベーストランザクションなどの同期技術を使用します。これらの仕組みにより、共有データを一度に変更できるスレッドを1つだけにするか、干渉なしに安全に操作を実行できるようにします。
質問セクション
1 / 5
このレッスンはプレミアムコンテンツです
プレミアムにアップグレードしてぼかしを解除し、全文を読めるようにしましょう。