RubyやPythonのGVL(GIL)は1プロセス1コアではない
概要
普段 Ruby を使ってアプリケーションを開発しているのですが、GVL (Python においては GIL)について勘違いしていた部分があったため、記事として整理します。
私は当初、「GVLがあるため、Rubyの1プロセスは1コアしか使えない」と雑に理解していました。そのため、複数コアのサーバでCPU負荷の高い処理を複数スレッドで動かしても、
- CPU コア 1: 100%
- CPU コア 2: 0%
のようになるはず、と理解していました。
下図のようなイメージです。

しかし、実際に試したところ、2コアのサーバでCPU使用率が 50% / 50% と、複数のコアに分散される結果となりました。
先にこの挙動の原因を述べると、コアの選択は OS によって行われており、GVL は「CPU を使うか否か」の制御だけを行っているためでした。
下図のようなイメージです。

このような挙動になるメカニズムを、実際に開発途中で起こったことをもとに解説していきます。
ことの発端
ある開発中のバッチ処理で、1プロセスのCPU負荷の検証を行う機会がありました。
検証環境:
- サーバ: 2 CPU コア
- プロセス: 1 Ruby プロセス
- スレッド: 複数 (IO ではなく、CPU寄りの処理が多め)
当初の予想
私の当初の想像は、「GVLはVM全体で1つのロックであり、Rubyのコードを実行できるスレッドは常に1つだけ」という知識に基づいていました。
以下の GVL や 並行処理に関する有名な記事では、1プロセス1コアであるというような表記があります。
想定される挙動としては、以下です。
- OSが 1プロセス複数スレッド を コア 1 に集中的に割り当てる。
- コア 1が スレッド A → スレッド B → スレッド C... とGVLを切り替えながら処理する。
- 結果、CPU使用率は
コア 1: 100%,コア 2: 0%になるはず。

よって以下のような使用率になるはずです。
- CPU コア 1: 100%
- CPU コア 2: 0%
観測結果:
このRubyプロセスでCPUを使い切る処理を走らせたところ、top コマンドでのCPU使用率は以下のようになりました。
- CPU コア 1: 50.0%
- CPU コア 2: 50.0%
(なぜ・・・・?)
実際の観測 (50% / 50%)
しかし、現実は前述の通り 50% / 50% でした。 これは、当初の想像とは異なり、OSが2つのコアに処理を分散させていることを示唆しています。
なぜ、GVLによって「同時に1スレッドしか実行できない」はずなのに、負荷が2つのコアに分散するのでしょうか?
実際の処理と誤解
この現象の鍵は、OSのスケジューラ と RubyのGVLという、異なるレイヤーの仕組みが、お互いを直接知ることなく動作している点にあります。
Ruby のスレッドスケジューリングはネイティブスレッドのそれを利用しています。よって詳細はプラットフォームに依存します。
https://docs.ruby-lang.org/ja/latest/doc/spec=2fthread.html
多くの解説で「GVLがあるため、Rubyは複数コアの恩恵を受けられない」と説明されます。これは正しいのですが、若干誤解を招く表現です。
これは、「複数コアが使えない」というわけではなく、「複数コアを使うが、同時にスレッドを実行できるコアは1つだけ」、つまり、「複数コアを効率的に使えない」という表現が正しいです。
図で示すと、以下のように、スレッドがコンテキストスイッチするたびに、OSによって CPUコアが選択されます。

1. OS (カーネル) の仕事
- OSは、Rubyプロセスが「実行したい」と要求しているスレッドを認識します。
- OSは、利用可能な2つのCPUコアに、これらのスレッドを(OSのポリシーに基づき)公平に割り当てようとします。
- OSは、Ruby VMの内部に「GVL」というプライベートなロックが存在することを知りません。
- そのため、OSは スレッド A を コア 1 に、スレッド B を コア 2 に、という形で同時に割り当てます。
2. Ruby GVL の仕事
- OSによってコア 1とコア 2に割り当てられた スレッド A と スレッド B ですが、次にRuby VM内のGVLの関門を通ります。
- GVLのルールは「どのコアで実行されているかに関わらず、Rubyのバイトコードを実行できるのは同時に1スレッドだけ」です。
- 仮に スレッド A が GVLを獲得したとします。
3. 50% / 50% が生まれる瞬間
top などのモニタリングツールは、この超高速の切り替えを平均値として表示します。
- コア 1: 時間の半分は「実行」、半分は「GVL待ち」。 → 平均 50% 使用
- コア 2: 時間の半分は「GVL待ち」、半分は「実行」。 → 平均 50% 使用
つまり、Rubyプロセスとしては合計1コア分 (50% + 50% = 100%) の仕事しか達成できていないにもかかわらず、OSのスケジューラによって、その負荷が2つのコアに均等に分散されて「見えていた」というのが真相です。

まとめ
今回の検証で、GVLに関する私の理解がアップデートされました。
誤解:
- GVLがあると、RubyプロセスはOSによって1つのCPUコアに固定され、そのコアだけが100%になる。
実際:
- GVLがあっても、OSはスレッドを複数のコアに割り当てる。ただし、Rubyコードを実行できるのは常に1スレッドだけ。
- GVLを待っている他のスレッドもCPUを消費するため、top 上では負荷が複数のコアに分散されて見える (例: 2コアで 50% / 50%)。
この事実は、特にパフォーマンスモニタリングにおいて重要です。
もし2コアサーバで1つだけRubyプロセスが動いており「CPU 50% / 50%」を示していた場合、
「CPUを使い切れていない」と解釈するのではなく、
「RubyはCPUを使い切っているがOSによって分散されている」という解釈が正しい、ということになります。
参考
Discussion