並行処理をマスターする:マルチプロセス・マルチスレッド・イベントループの違いと選び方
並行処理のマスタリング
あなたが開発したアプリには、同時に複数のユーザーがアクセスします。そして、誰もができるだけ速いレスポンスを求めています。そのためには、何らかの方法で並行性(コンカレンシー)に対応する必要があります。でも心配は無用です。多くのWebサーバーはデフォルトで並行処理に対応しています。ただし、アプリをスケールさせる段階になると、並行性を最も効率的な形で活用することが求められます。
並行処理の主な方式
並行処理を実現する方法は主に3つあります。マルチプロセス、マルチスレッド、イベント駆動です。それぞれに適した用途やメリット・デメリットがあります。この記事では、それぞれの違いと、どのような場面でどれを使うべきかを解説します。
マルチプロセス(Unicorn)
これは最もシンプルな並行処理の方式です。マスタープロセスが自身を複数のワーカープロセスへフォークし、実際のリクエスト処理は各ワーカーが担当し、マスターはワーカーの管理を行います。
各ワーカープロセスはコードベース全体をメモリ上に保持します。そのため、この方式はかなりメモリを消費し、大規模なインフラへのスケールが難しくなるという弱点があります。
| マルチプロセスのまとめ | |
|---|---|
| 活用例 | Ruby以外の身近な例としてはChromeブラウザが挙げられます。Chromeはマルチプロセスの並行処理を採用し、各タブに独立したプロセスを割り当てています。これにより、1つのタブがクラッシュしてもアプリケーション全体が落ちることはありません。また、悪意あるコード(エクスプロイト)の影響を1つのタブ内に隔离できるというセキュリティ上の利点もあります。 |
| メリット | 実装が最もシンプル。 スレッドセーフティの問題を考慮しなくてよい。 個々のワーカーがクラッシュしてもシステム全体への被害が限定的。 |
| デメリット | 各プロセスがコードベース全体をメモリに読み込むため、メモリ消費が大きい。 その結果、大量の同時接続にはスケールしにくい。 |
マルチスレッド(Puma)
このスレッディングモデルでは、1つのプロセスが複数のリクエストを同時に処理できます。仕組みとしては、単一のプロセス内で複数のスレッドを実行します。
マルチプロセス方式とは異なり、すべてのスレッドは同じプロセス内で動作します。つまり、グローバル変数などのデータを共有します。そのため、スレッドごとの追加メモリ消費はわずかで済みます。
GIL(グローバルインタプリタロック)
ここでMRI(CRuby)におけるグローバルインタプリタロック(GIL)について触れておきましょう。GILとは、すべてのRubyコードの実行を囲むロックのことです。スレッドが並列に動いているように見えても、実際にアクティブになれるスレッドは常に1つだけです。
一方、I/O処理はGILの外側で実行されます。データベースクエリの結果を待っている間はロックされないため、その間に別のスレッドが処理を進めることができます。しかし、スレッド内で大量の計算処理やハッシュ・配列への操作を行う場合、MRIでは1コアしか活用できません。多くの場合、マシンの性能を最大限に引き出すには複数プロセスの併用が必要です。あるいは、GILを持たないRubiniusやJRubyを選択するという手もあります。
スレッドセーフティ
複数のスレッドを使う場合は、共有データを扱うすべてのコードをスレッドセーフな形で書くよう注意が必要です。たとえば、Mutexを使って共有データ構造を操作前にロックすることで実現できます。こうすることで、自分がデータを書き換えている間に、他のスレッドが古いデータをもとに処理を進めてしまう事態を防げます。
| マルチスレッドのまとめ | |
|---|---|
| 活用例 | 「中間的な」選択肢と言えます。短いリクエストを大量にさばく必要がある、一般的なWebアプリケーション(アクセスの多いWebサービスなど)に向いています。 |
| メリット | マルチプロセスよりもメモリ消費が少ない。 |
| デメリット | コードがスレッドセーフであることを保証する必要がある。 スレッドでクラッシュが起きると、プロセス全体が落ちる可能性がある。 GILにより、I/O以外の処理はロックされる。 |
イベントループ(Thin)
イベントループは、大量の同時I/O処理が必要な場面で活躍します。このモデル自体は複数のリクエストを厳密に同時実行するものではありませんが、多数の同時接続ユーザーを効率的に捌く手法として優れています。
以下はRubyで書かれた非常にシンプルなイベントループの例です。ループはevent_queueからイベントを取り出して処理します。イベントが存在しなければスリープし、再びキューに新しいイベントがないか確認します。
loop do
if event_queue.any?
handle_event(event_queue.pop)
else
sleep 0.1
end
end図解バージョン
次の図解では、もう一歩踏み込んでみましょう。イベントループがOS、キュー、そしてメモリと美しいダンスを踊る様子をイメージしてください。
ステップごとの流れ
- OSがネットワークとディスクの利用可能状態を監視します。
- I/Oの準備が整うと、OSがキューへイベントを送信します。
- キューはイベントのリストであり、イベントループは先頭のイベントを取り出します。
- イベントループがそのイベントを処理します。
- 接続に関するメタデータを保存するために、いくらかのメモリを使用します。
- イベントの内容に応じて、新しいイベントを直接イベントキューへ投入することもあります。たとえば、イベントの内容に基づいてキューをシャットダウンする指示などを送れます。
- I/O操作を行いたい場合は、OSに対して特定のI/O操作に関心があることを伝えます。OSはネットワークとディスクの監視([1]を参照)を続け、I/Oの準備ができたら再度イベントを追加します。
| イベントループのまとめ | |
|---|---|
| 活用例 | ユーザーとの同時接続数が非常に多いサービス。Slackのようなチャットツールや、Chromeの通知機能などを思い浮かべるとよいでしょう。 |
| メリット | 接続あたりのメモリオーバーヘッドがほぼゼロ。 膨大な数の並列接続にもスケールできる。 |
| デメリット | 理解が難しいメンタルモデルである。 キューが溜まりすぎないよう、バッチサイズを小さく予測可能に保つ必要がある。 |
どれを選ぶべきか?
この記事を通じて、さまざまな並行処理モデルへの理解が深まったなら幸いです。開発者にとって習得が難しいテーマのひとつですが、きちんと理解しておけば、自分のアプリに最適な構成を試験的に導入し、判断する力が身につきます。
まとめ
- ほとんどのアプリにとってはマルチスレッドが合理的な選択です。Ruby/Railsエコシステムも(緩やかにではありますが)この方向へ移行しつつあります。
- 長時間持続するストリームを含む高並行性アプリを運用しているなら、イベントループがスケールの鍵となります。
- トラフィックがそれほど多くないサイトや、ワーカーのクラッシュを許容したい場合は、安定のマルチプロセスが頼りになります。
さらに、イベントループをスレッドの中で動かし、そのスレッドをマルチプロセス構成の中で実行することも可能です。つまり、stroopwafel(シロップワッフル)を「持って」かつ「食べる」こともできるのです!
これらの並行処理モデルについてさらに詳しく知りたい方は、マルチプロセス、マルチスレッド、イベントループに関する詳細記事もぜひご覧ください。
-
Rubyで学ぶUNIXデーモンの仕組み――理論と実装のステップバイステップ解説
UNIXデーモンとは、バックグラウンドで動き続けるプログラムのことです。Nginx、PostgreSQL、OpenSSHなどがその代表例として挙げられます。デーモンはいくつかの特殊なテクニックを使ってプロセスを「切り離し(detach)」し、どの端末からも独立した状態で動作できるようにしています。 筆者は昔からデーモンに何かと魅了されてきました(名前の響きのせいかもしれません)。そこで今回は、デーモンがどのように動作するのか、そしてRubyでどうやって作れるのかを解説します。 ……ただし、その前に。 本番環境では真似しないでください! 正直なところ、自分でデーモンを作りたい場面はほとん
-
Rubyの正規表現(Regex)を徹底解説!基礎から実践テクニックまで
Rubyの正規表現(ruby regex)は、文字列の中から特定のパターンを見つけ出し、データを抽出してさらに処理を行うための強力なツールです。 正規表現の代表的なユースケースとしては、大きく分けて「バリデーション(検証)」と「パース(解析)」の2つがあります。 たとえば、メールアドレスを考えてみましょう。Rubyの正規表現を使えば、「有効なメールアドレスとはどのような形式か」を定義できます。つまり、プログラムが有効なメールアドレスと無効なものを自動的に判別できるようになるのです。 Rubyでは、正規表現を2つのスラッシュ(/)で囲んで定義します。これにより、他の構文と区別されます。最もシン