Java

グリーンスレッドとは?OSスレッドとの違いとN:1モデルの制約を実測で解説

グリーンスレッドとは?OSスレッドとの違いとN:1モデルの制約を実測で解説

グリーンスレッド(green threads)は、OSのカーネルではなく、言語のランタイムやライブラリがスケジュールするユーザーモードのスレッドです。本記事では主に、複数の処理を1本のOSスレッドで動かすN:1方式を扱います。ただし、グリーンスレッドにはM:N方式の実装もあります。起動が軽く、本数も増やしやすい一方で、1本がブロックすると全体が止まり、マルチコアも使えません。

この記事では、N:1・1:1・M:Nの3つのスレッドモデルでグリーンスレッドの位置を整理し、JavaやRubyがグリーンスレッドを手放した経緯と、goroutineや仮想スレッドとの関係を説明します。N:1モデルの制約は、Pythonの最小スケジューラとgeventを実際に動かして確かめ、その出力も掲載しています。

まとめ:グリーンスレッドの要点と選び方

  • 定義:OSではなくランタイムがスケジュールするスレッドです。初期のJavaやCRuby 1.8の実装は、複数のスレッドを1本のOSスレッドに載せるN:1(M:1)モデルでした。一方、1.0以前のRustのようにM:Nで動かす実装もありました。
  • 強み:起動と切り替えが軽いことです。macOSでの実測では、2,000本の起動にかかった時間はOSスレッドの約6分の1でした。
  • 弱み:N:1方式では、OSスレッド自体をブロックする呼び出しが他の処理も止めます。geventのような協調的実装では、制御を譲らないCPU処理も他の処理を待たせます。1本のOSスレッドで動くので、マルチコアも使えません。
  • 歴史:初期のJava(Solaris版)とCRuby 1.8はN:1でしたが、どちらもOSスレッドとの1:1対応へ移行しました。Rustも1.0より前にグリーンスレッドを標準ライブラリから外しています。
  • 現在:goroutine、Javaの仮想スレッド、ErlangのプロセスはM:Nモデルです。性能や障害を議論するときは、N:1のグリーンスレッドと区別して扱うのが正確です。
  • 選び方:I/O待ちが中心の処理ならユーザーモードのスレッドが有効です。ただし新しく採用するなら、N:1のライブラリよりM:Nのランタイム(Go、Java 21以降)を選ぶほうが安全です。

グリーンスレッドの定義とOSスレッドとの違い

OSスレッドは、カーネルが生成し、カーネルのスケジューラが実行するCPUを割り当てます。切り替えはカーネルが行い、システムコールによる待機だけでなく、タイマ割り込みなどを契機とするプリエンプションでも発生します。グリーンスレッドはこの仕組みをユーザー空間で再現したもので、スタックやレジスタの退避と復元、次に動かす処理の選択を、ランタイムが自分で行います。

JEP 444(Java 21で正式化された仮想スレッドの仕様)は、初期のJavaのグリーンスレッドを「Java’s green threads all shared one OS thread (M:1 scheduling)」と説明しています。複数のスレッドが1本のOSスレッドを共有する、という点がこの用語の核心です。

観点 グリーンスレッド(N:1) OSスレッド(1:1)
スケジュールする主体 言語ランタイム・ライブラリ OSカーネル
切り替えの契機 実装依存(geventは協調的) カーネルが割り込む(プリエンプティブ)
マルチコアの利用 不可(OSスレッドが1本) 可能
OSスレッドをブロックする呼び出し 同じOSスレッド上の処理が止まる そのスレッドだけが止まる
本数の上限 メモリ量に依存 OS・資源制限に依存(記事の検証環境では4,096本)

非同期処理全体の中でスレッド・コルーチン・イベントループがどう位置づけられるかは、非同期処理とは?同期処理との違いから実装方式まで実装者目線で解説で整理しています。仮想スレッドという用語の一般的な定義は仮想スレッドの概要と基本的な定義についてで扱っています。

N:1・1:1・M:Nのスレッドモデルとグリーンスレッドの位置

言語側のスレッドとOSスレッドの対応は、N:1・1:1・M:Nに大別できます。本記事ではN:1方式を中心に説明しますが、Rustの旧libgreenのようにM:N方式のグリーンスレッドもあります。

  • N:1:N本のスレッドを1本のOSスレッドで動かします。切り替えは軽いものの、マルチコアを使えず、1本がブロックすると全体が止まります。
  • 1:1:言語のスレッド1本にOSスレッド1本を割り当てます。マルチコアを使え、ブロッキングの影響もそのスレッドだけで済みます。ただし、生成と切り替えのコストはOSスレッドと同じです。
  • M:N:M本のスレッドをN本のOSスレッドに載せ替えながら動かします。N:1の軽さと1:1のマルチコア対応を両立できる反面、ランタイムの実装が最も複雑になります。

OSのスレッドライブラリ自体も1:1に落ち着いています。Linuxでは、glibc 2.3.2からNPTLが使えるようになり、LinuxThreadsはglibc 2.4でサポート対象から外れました。man7.orgのpthreads(7)は、この2つを「Both of these are so-called 1:1 implementations」と説明しています。Solarisは、Solaris 9で導入した新しいスレッドライブラリで、各スレッドに1つのLWPを割り当てる方式に切り替えました。

協調的な切り替えの仕組み|Pythonで動かす最小スケジューラ

ここで示す協調的スケジューリングは、「処理が自分で制御を手放し、スケジューラが次の処理へ渡す」という流れです。Pythonのジェネレータを使うと、この流れを十数行で再現できます。yieldが制御を譲る地点で、runがラウンドロビンで次のタスクを選ぶスケジューラにあたります。

from collections import deque

def worker(name, n):
    for i in range(n):
        print(f"{name}: {i}")
        yield  # ここで制御をスケジューラへ返す

def run(tasks):
    ready = deque(tasks)
    while ready:
        task = ready.popleft()
        try:
            next(task)
            ready.append(task)
        except StopIteration:
            pass

run([worker("A", 3), worker("B", 2)])

Python 3.14.6で実行した出力は次のとおりです。OSスレッドは1本のままで、AとBが交互に進んでいます。

A: 0
B: 0
A: 1
B: 1
A: 2

実際のライブラリは、ジェネレータの代わりに各スレッドのスタックとレジスタを退避・復元します。Pythonのgreenletは、C拡張でスタックを切り替えて同じことを実現しています。

N:1モデルの制約を実測|geventでのブロッキングとCPU占有

ここからは、greenletを土台にしたgeventで、N:1モデルの制約を実際に確かめます。gevent公式ドキュメントは「The greenlets all run in the same OS thread and are scheduled cooperatively.」と説明しています。実行環境は、macOS 26.6.2、Intel Core i9-9880H、Python 3.14.6、gevent 26.9.0、greenlet 3.5.6です(geventとgreenletは2026年9月17日時点のPyPI最新版)。

ブロッキング呼び出しによる全体停止

1秒待つ処理を3本同時に起動し、全体の所要時間を計ります。待ち方は、geventのgevent.sleepと標準ライブラリのtime.sleepの2通りです。

import time
import gevent

def io_task(sleep):
    sleep(1)

def run(label, sleep):
    t = time.perf_counter()
    gevent.joinall([gevent.spawn(io_task, sleep) for _ in range(3)])
    print(f"{label}: {time.perf_counter() - t:.2f}s")

run("gevent.sleep", gevent.sleep)
run("time.sleep (パッチなし)", time.sleep)
gevent.sleep: 1.15s
time.sleep (パッチなし): 3.29s

繰り返し実行したところ、gevent.sleepは毎回1.15秒前後、time.sleepは3.29〜3.45秒でした。time.sleepはOSスレッドごと眠らせるため、他のgreenletに順番が回らず、3本が直列に実行されます。先頭でgevent.monkey.patch_all()を呼んで標準ライブラリを協調版に置き換えると、同じtime.sleepでも1.15秒で終わりました。裏を返すと、置き換えの対象外になるC拡張のブロッキングI/O(一部のDBドライバなど)が1つあるだけで、並行性は失われます。

Celeryのワーカーをgeventプールで動かす構成もこの前提の上にあります(Celery(Python)とは:分散タスクキューの仕組みとブローカー選定・冪等性設計を解説)。

CPUを占有する処理による待ち

次は、0.1秒ごとに時刻を表示するgreenletと、CPUを使い続ける計算処理を同時に動かします。

import time
import gevent

def ticker():
    start = time.perf_counter()
    for _ in range(3):
        gevent.sleep(0.1)
        print(f"tick {time.perf_counter() - start:.2f}s")

def cpu_bound():
    sum(i * i for i in range(20_000_000))

gevent.joinall([gevent.spawn(ticker), gevent.spawn(cpu_bound)])
tick 4.31s
tick 4.56s
tick 4.81s

0.1秒後に出るはずの最初の表示が、4.31秒後になりました(6回の実行で4.07〜4.70秒)。計算処理は途中で制御を譲らないため、終わるまでスケジューラが動けません。協調的な切り替えでは、「すぐに終わらない処理はグリーンスレッドに載せない」という規律を守るのはアプリケーション側の責任です。

起動コストと本数の上限

2,000本のOSスレッドと2,000本のgreenletを起動し、所要時間と最大常駐メモリ(ru_maxrss)の増分を3回ずつ比べました。どちらも生成前から計測を始め、起動した各スレッドはイベント待ちで止めています(OSスレッドはthreading.Event、greenletはgevent.event.Event)。待機の実装が違うため、厳密な性能比ではなく傾向を見るための参考値です。

2,000本の起動 所要時間 最大常駐メモリの増分
OSスレッド(threading) 0.47〜0.53秒 30.9〜31.0MiB
greenlet(gevent) 0.06〜0.08秒 15.2〜15.3MiB

起動時間は約6分の1になりましたが、メモリの差は約2倍にとどまりました。この比較は予約されたスタック領域ではなく、実際に常駐したメモリを測っています。スタックの使用量に加え、各実装の管理オブジェクトや待機処理も測定値に影響します。この環境で差を決めたのは、メモリよりも本数の上限でした。OSスレッドを増やし続けると、4,095本を起動した次の1本でRuntimeError: can't start new threadが発生しました。メインスレッドを含めると4,096本で、sysctl kern.num_taskthreadsの値(4096)と一致します。greenletは10万本を起動でき、そのときの増分は747.5MiB(1本あたり約8KB)でした。

Java・Ruby・Rustがグリーンスレッドを手放した経緯

Java:Solaris版のM:1からネイティブスレッドへの移行

OracleのSolaris向け開発者ガイドは、「The initial implementation of Java threads on the Solaris system was many-to-one.」と記しています。Java 2 SDK v1.2のJNI FAQによると、Solaris版のjavaコマンドは既定でgreen threadsを使っていました。その後、Solaris 2.6以降のプラットフォームでは、性能向上のためにgreen threadsライブラリがSolarisのネイティブスレッドに置き換えられています。JEP 444はこの流れを、グリーンスレッドが「eventually outperformed by platform threads」、つまりOSスレッドのラッパーに性能で抜かれたとまとめています。

Java 21の仮想スレッドは、ユーザーモードのスレッドをM:Nで復活させたものです。スケジューラはFIFOモードのForkJoinPoolで、並列度は既定で利用可能なプロセッサ数です。使い方とpinningの注意点はJava Virtual Thread(仮想スレッド)とProject Loomとは?作り方・仕組み・落とし穴を実務目線で解説で解説しています。

Ruby:1.8のM:1・1.9の1:1+GVL・3.3のM:N導入

CRubyは1.8まで、複数のRubyスレッドを1本のネイティブスレッドで動かすM:1方式でした。Ruby 1.9でRubyスレッドとネイティブスレッドを1:1に対応させ、同時にGVL(Global VM Lock)を導入しています。その後、Ruby 3.0(2020年12月25日)でブロッキング処理を横取りするFiber Schedulerが入りました。Ruby 3.3ではM:Nスレッドスケジューラが追加されていますが、C拡張との互換性を壊すおそれがあるため、メインRactorでは既定で無効です。有効にするには環境変数RUBY_MN_THREADS=1を指定します。

Rust:1.0公開前の組み込みランタイム削除

Rustは、2014年9月16日開始のRFC 230で、標準ライブラリに組み込まれていたランタイムを削除し、グリーンスレッド実装のlibgreenをツリー外のパッケージへ移しました。RFC 230によると、当時のRustのグリーンスレッドはタスクをM:Nでスケジュールする方式でした。理由として挙げられたのは、ネイティブスレッドとグリーンスレッドに共通のI/O APIを強制する設計上の結合、バイナリサイズや動的ディスパッチのオーバーヘッド、ブロッキングI/Oとの相互運用の難しさ、保守の負担です。Rust 1.0(2015年5月15日)はグリーンスレッドを持たない状態で公開され、軽量な並行処理はasync/awaitとTokioなどの外部ランタイムが担うようになりました。

goroutine・仮想スレッド・Erlangプロセスとグリーンスレッドの線引き

goroutineや仮想スレッドを「グリーンスレッド」と紹介する記事は少なくありません。ユーザーモードのスレッドという点では同じ仲間ですが、本記事の立場は、設計や技術選定の文脈ではN:1の実装と区別すべきというものです。JEP 444自身が初期JavaのM:1のグリーンスレッドとM:Nの仮想スレッドを分けて説明しており、両者ではマルチコアの利用とブロッキング時の挙動、つまりN:1の弱点そのものが違うからです。

実装 モデル 切り替え ブロッキング時
初期Java(Solaris)・CRuby 1.8 N:1 ランタイムが制御 全体が止まる
gevent(greenlet) N:1 協調的 パッチ対象外なら全体が止まる
Go goroutine M:N 非同期プリエンプション(Go 1.14〜) ランタイムが他のOSスレッドへ逃がす
Java仮想スレッド M:N 対応する待機操作でOSスレッドから外れる pinning・一部のブロッキング操作で占有
Erlangプロセス M:N reduction数でプリエンプト 他のスケジューラスレッドは動き続ける

「グリーンスレッド」を広い意味での「ユーザーモードスレッド」として使うこと自体は誤りではありません。ただ、その呼び方をすると、ブロッキングで全体が止まる、マルチコアを使えない、という前提まで引き継いだように読まれます。性能や障害の議論では、N:1かM:Nかを明記するほうが誤解を生みません。

グリーンスレッドを採用すべき場面と避けるべき場面

ユーザーモードのスレッドが効くのは、I/O待ちが大半を占める処理を大量に並べる場面です。実測でも、起動時間と本数の上限にははっきり差が出ました。一方、採用の可否はモデルによって変わります。

  • 採用してよい:数千本以上の接続を同時に待つサーバーやクローラーのうち、I/Oがすべて協調版に置き換わっていることを確認できたもの。既存の同期コードをほぼそのまま並行化したい場合は、geventのパッチ方式が近道です。
  • 避けるべき:CPUを使う処理が混ざるワークロード(前の計測では、他の処理が4秒以上待たされました)、パッチの効かないC拡張ドライバを使う構成、マルチコアで速くしたい計算処理。
  • 新規採用ならM:Nを優先:これから軽量な並行処理を設計するなら、N:1のライブラリより、Goや、Java 21以降の仮想スレッドのようなM:Nのランタイムを候補にすると、複数のOSスレッドを利用できます。ただし、CPU処理による実行枠の占有や、非対応のブロッキング処理への対策は別途必要です。Pythonでasyncio系を選ぶ場合の比較はanyio・Trio・asyncioの違いと使い分け|Python非同期ライブラリ比較にまとめています。

よくある質問

グリーンスレッドとは簡単に言うと何ですか?

OSではなく言語のランタイムが切り替えを管理するスレッドです。初期のJavaなどのN:1実装では、OSから見えるのは1本のスレッドだけで、その中で複数の処理が順番に動きます。起動は軽い一方、OSスレッドをブロックする処理が1つあると全体が止まります。

green threadとnative threadの違いは何ですか?

スケジュールする主体が違います。green threadはランタイムがユーザー空間で切り替え、native thread(OSスレッド)はカーネルが切り替えます。native threadはマルチコアを使えて、ブロッキングの影響もそのスレッドだけで済みます。

Javaでグリーンスレッドが使われなくなったのはなぜですか?

初期のJava(Solaris版)のグリーンスレッドは、全スレッドが1本のOSスレッドを共有するM:1方式でした。JEP 444によると、OSスレッドのラッパーであるプラットフォームスレッドに性能で抜かれています。Solaris 2.6以降では、性能向上のためにネイティブスレッドへ置き換えられました。

goroutineはグリーンスレッドですか?

ユーザーモードのスレッドという点では同じ仲間ですが、goroutineは複数のOSスレッドに載せ替えながら動くM:Nモデルです。初期のJavaやCRuby 1.8のように1本のOSスレッドに限られるN:1のグリーンスレッドとは、マルチコアの利用とブロッキング時の挙動が異なります。

Javaの仮想スレッドはグリーンスレッドの復活ですか?

ユーザーモードのスレッドが戻ってきた、という意味では復活と言えます。ただし、仮想スレッドはM:Nでスケジュールされ、スケジューラの並列度は既定で利用可能なプロセッサ数です。M:1だった初期のグリーンスレッドとは構造が違います。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.09 テックブログ IDCFクラウド(IDCフロンティア)不正アクセス・ランサムウェア:影響先・復旧・データは戻るか
  2. 2026.10.09 テックブログ ニッスイのサイバー攻撃で日水物流の入出荷停止|委託先クラウド障害に荷主が備える手順
  3. 2026.10.09 テックブログ 京王電鉄のランサムウェア被害とグループ共通基盤:決済・ポイント・予約が止まった範囲と遮断の初動
  4. 2026.07.15 コラム LINEミニアプリとは?未認証と認証済みの違い・LIFFとの使い分け・審査と開発費用【2026年版】
  5. 2026.10.08 テックブログ 手間いらずの不正アクセスと宿泊予約フィッシング|施設・予約者・接続先の点検手順

RELATED POSTS 関連記事

目次