🌏

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コアであるというような表記があります。
https://techracho.bpsinc.jp/hachi8833/2025_06_09/151182

想定される挙動としては、以下です。

  1. OSが 1プロセス複数スレッド を コア 1 に集中的に割り当てる。
  2. コア 1が スレッド A → スレッド B → スレッド C... とGVLを切り替えながら処理する。
  3. 結果、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によって分散されている」という解釈が正しい、ということになります。

参考

https://b.hatena.ne.jp/entry/s/engineering.dena.com/blog/2026/03/sidekiq-huge-ranking-aggregation/

Discussion