Python 3.14のfree-threadingでGILを外す—効果と使いどころ

Python 3.14のfree-threadingでGILを外す—効果と使いどころ | mohablog

Python 3.14でfree-threadingビルドが正式サポートになりました。GILが外れたと聞くと「asyncioを書かずにスレッドだけで速くなる」と読みたくなります。ただ効くのは純PythonのCPUバウンドな並列処理に限られ、IO待ちの並行処理は今もasyncioの担当です。

目次

free-threadingが「正式サポート」になった意味

Python 3.13までfree-threadingは実験扱いでした。3.14(3.14.0、2025年10月リリース)で位置づけが変わります。

experimentalが外れたPEP 779

「What’s New in Python 3.14」のハイライトに “PEP 779: Free-threaded Python is officially supported” が並びました。公式の言い回しは “officially supported but still optional”。デフォルトのビルドは今もGIL有効で、free-threadingは選んで使う phase II の段階です。実験フラグは外れた、ただし標準ではない。インストール時に明示して選ばないと、free-threadingビルドは手元に入りません。

python3.14t という別バイナリ

free-threadingは通常のpython3.14とは別のバイナリとして配布されます。末尾にtが付いたpython3.14tがそれ。同じ3.14でもGIL有効ビルドと無効ビルドは別物で、共存できます。「3.14にしたらGILが消えた」わけではない点に注意してください。

手元のPythonがfree-threadedか確かめる

公式howtoの “Identifying free-threaded Python” セクションが、判定方法を2つ挙げています。

sys._is_gil_enabled() と Py_GIL_DISABLED

実行時の状態とビルドの素性は別物。混同すると、GILが戻った状態を「free-threading非対応」と誤読します。

import sys
import sysconfig

# 実行時にGILが動いているか(再有効化されていないか)
print(sys._is_gil_enabled())
# このビルドがfree-threadingに対応しているか(推奨される判定)
print(sysconfig.get_config_var("Py_GIL_DISABLED"))

python3.14tで動かした結果です。

False
1

sys._is_gil_enabled()は実行時の状態、Py_GIL_DISABLEDはビルドの素性を表します。公式はビルド判定には後者を推奨。前者は後述のGIL再有効化で値が変わるためです。

uvで入れるのが速い

入手経路は3つ。公式インストーラ(macOS/Windowsは追加コンポーネントのチェックで導入)、ソースからの./configure --disable-gil、そしてuvです。uvなら1行で済みます。

uv python install 3.14t
uv run --python 3.14t python -c "import sys; print(sys._is_gil_enabled())"
False

CPUバウンドな処理を並列で実測する

通常のCPythonは、純Pythonのループを複数スレッドへ分けてもGILが1つずつしか実行させません。GILは1個きりのロックで、どのスレッドもPythonバイトコードを動かす前に必ず取得する。だから4スレッド起動しても、進むのは実質1コアぶん。free-threadedビルドはこのロックを撤廃し、スレッドが別々のコアで同時にバイトコードを実行できるようにします。

同じコードをGIL有効・無効で回す

純Pythonの数値ループを、スレッド数を変えて回すベンチを用意しました。NumPyのようなCレベルでGILを解放する処理ではなく、わざとPythonの実行で詰まらせる形にしています。

import sys
import time
from concurrent.futures import ThreadPoolExecutor

def cpu_task(n: int) -> int:
    total = 0
    for i in range(n):
        total += i * i % 7
    return total

def run(workers: int, total_iters: int) -> float:
    chunk = total_iters // workers
    start = time.perf_counter()
    with ThreadPoolExecutor(max_workers=workers) as ex:
        list(ex.map(cpu_task, [chunk] * workers))
    return time.perf_counter() - start

if __name__ == "__main__":
    print("GIL:", sys._is_gil_enabled())
    iters = 200_000_000
    for w in (1, 4, 8):
        print(f"{w} threads: {run(w, iters):.2f}s")

同じスクリプトをpython3.14(GIL有効)とpython3.14t(GIL無効)で実行した結果です。

$ python3.14 bench.py
GIL: True
1 threads: 3.80s
4 threads: 3.90s
8 threads: 4.01s

スレッドを増やしたときの伸び

8コアのLinux(x86-64、Python 3.14.0)で測った値を並べます。GIL有効ビルドはスレッドを増やしても頭打ち、free-threadedはコア数なりに伸びました。

スレッド数python3.14(GIL有効)python3.14t(GIL無効)
13.80s4.12s
43.90s1.31s
84.01s0.92s

GIL有効ビルドは4スレッドでも3.90sで、1スレッドの3.80sからほぼ動きません。free-threadedは8スレッドで0.92s、1スレッド比で約4.5倍。8コアぶんがそのまま効いています。公式howtoの言葉を借りれば、”Free-threaded execution allows for full utilization of the available processing power by running threads in parallel on available CPU cores” がそのまま数字に出た形です。ただし純Pythonのループ限定。NumPyやPolarsのように内部でGILを解放するライブラリは、GIL有効ビルドでもとっくに並列で動いています。

multiprocessingと何が違うか

この並列化、これまではmultiprocessingで別プロセスを立てて回避してきました。ただプロセス間はメモリを共有しないので、渡すデータはpickleで直列化され、ワーカー起動のコストもかかります。free-threadingは同一プロセス内のスレッドなので、巨大な共有データを各スレッドが直接読めます。数GBのモデルや辞書をワーカーごとに複製していたなら、ここが効く。pickleにできないオブジェクトをスレッドへ渡せる点も差です。

free-threadingが効かない処理

速くなるのはCPUを使い切る処理だけ。検索サジェストに「python 3.14 gil fastapi」が出るあたり、Web APIの高速化を期待する向きがありますが、ここは見込み違いになりがちです。

IO待ちはasyncioのまま

HTTPリクエストやDBクエリの待ち時間は、CPUをほとんど使いません。レスポンスが返るまでスレッドが寝ているだけ。この待ち時間はGILの有無と無関係で、asyncioのイベントループが1スレッドで何百本もの接続を捌きます。FastAPIのIOバウンドなエンドポイントをfree-threadingへ置き換える理由はありません。

シングルスレッドは5〜10%遅くなる

GILを外した代償もあります。公式howtoの “Single-threaded performance” は “The performance penalty on single-threaded code in free-threaded mode is now roughly 5-10%, depending on the platform and C compiler used” と書いています。3.13より縮みましたが0ではない。先のベンチでも1スレッドが3.80s→4.12sと約8%遅くなっていました。並列で取り返せる処理かどうかが分かれ目です。

C拡張ライブラリとcp314tホイールの現状

free-threaded buildで困るのはたいてい依存ライブラリ側です。C拡張がGILなしで安全に動くと宣言していないと、恩恵を受けられません。

cp314t タグのホイール

free-threaded build向けのホイールはcp314tというABIタグを持ちます。GIL有効版のcp314とは別枠。python3.14t上ではpipもuvも自動でcp314tホイールを選びます。2026年5月時点の対応状況をざっと並べます。

ライブラリcp314t ホイール補足
NumPy配布あり2.1系から対応。並列計算の土台
Cython対応モジュール側で対応宣言が要る
pandas配布あり内部はNumPy依存
対応待ちの拡張未配布ありimportでGILが自動再有効化

未対応の拡張を読むとGILが戻る

free-threading対応を宣言していないC拡張をimportすると、CPythonは安全側に倒してGILを自動で再有効化し、警告を出します。せっかくpython3.14tで起動しても、依存に1つ未対応拡張が混ざると並列化が消える。明示的にGILを戻すなら環境変数かコマンドラインオプションです。

PYTHON_GIL=1 python3.14t app.py
# または
python3.14t -X gil=1 app.py
>>> import sys
>>> sys._is_gil_enabled()
True

この挙動があるので、ビルド素性の判定にはPy_GIL_DISABLED(変わらない)、実行時の実態確認にはsys._is_gil_enabled()(戻ると変わる)を使い分けます。依存をuv pip installした直後にこの2行で実態を確認しておくと安全です。cp314tホイールの無いライブラリは、ソースビルドにフォールバックするか、警告とともにGILを戻すか、どちらかになります。

本番に入れるかの判断

投入価値があるのは、純PythonのCPU処理がボトルネックで、かつ依存C拡張がcp314t対応済みのケース。画像の前処理、独自パーサ、シミュレーションのような計算をスレッドで分けられるなら、8コアで3〜4倍は現実的な伸びです。

逆にIOバウンド中心のWeb APIやバッチは、シングルスレッド5〜10%の劣化だけ受けて並列の恩恵が薄い。既存のIOバウンドなFastAPIアプリをpython3.14tへ載せ替えて速くしようとする使い方は外れます。スレッドが待つ時間はGILの有無で変わらないからです。一方、画像のリサイズを数百枚まとめてかけるバッチをThreadPoolExecutorで分けるなら、8コアで素直に縮みます。まず自分のワークロードを先のベンチ形式で測り、スレッドを増やして時間が縮むかを確かめてから決めてください。phase IIが「optional」のままなのは、こうした測定とライブラリ確認を各自でやる前提だからです。

まとめ

  • Python 3.14でfree-threadingが正式サポート(PEP 779)。別バイナリpython3.14tとして配布され、デフォルトは今もGIL有効
  • 効くのは純PythonのCPUバウンド並列。8コア実測で1スレッド比4.5倍、4スレッドでも約3倍
  • IO待ちは引き続きasyncioの担当。FastAPIのIO処理をfree-threadingで置き換えても速くならない
  • シングルスレッドは5〜10%のオーバーヘッド。並列で取り返せるかが投入の分かれ目
  • C拡張はcp314tホイール対応が前提。未対応をimportするとGILが自動で戻り、並列化が無効になる
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次