CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: uv を使って手軽にフリースレッド版の CPython を試す

CPython 3.14 から、フリースレッドモード (Free-threaded mode) が正式にサポートされた 1。 フリースレッドモードでは、従来の CPython に存在した GIL (Global Interpreter Lock) の制約が無くなる。 GIL の制約が無くなると、これまでは基本的にマルチプロセスを使っていた並列処理がマルチスレッドでも書けるようになる。

ただし、フリースレッドモードにはいくつか注意点がある。 まず、CPython のバージョン 3.14 ではフリースレッドモードがデフォルトで無効になっている。 有効にするには、CPython をビルドするときにオプション (--disable-gil) で明示的に指定しなければいけない 2。 そして、フリースレッドモードが有効なときはプログラムのシングルスレッド性能が 5 ~ 10% 程度低下する。 つまり、並列処理は書きやすくなる一方で逐次処理は少し遅くなる。

今回は uv 3 を使うことで手軽にフリースレッドモードが有効な CPython を試してみる。 フリースレッドモードが有効な CPython が、本当にマルチスレッドで CPU コアを使い切れるのかを確認する。

使った環境は次のとおり。

$ sw_vers      
ProductName:        macOS
ProductVersion:     15.7.1
BuildVersion:       24G231
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M3
$ sysctl hw.logicalcpu
hw.logicalcpu: 8
$ uv -V
uv 0.9.2 (Homebrew 2025-10-10)

もくじ

下準備

まずは uv と GNU time をインストールしておく。 CPU の状況を観測する手段が何かしらあれば GNU time は無くても構わない。

$ brew install uv gnu-time

uv でインストールする Python で、フリースレッドモードが有効なビルドには名前の末尾に t がつく。 なので、まずはフリースレッドモードが有効な CPython 3.14 である 3.14t をインストールしよう。

$ uv python install 3.14t

比較対象として、フリースレッドモードが無効な通常の CPython 3.14 もインストールしておこう。

$ uv python install 3.14

サンプルコード

次に、それぞれのビルドで実行するサンプルコードを示す。 サンプルコードでは CPU の論理コア数よりも 1 少ない数のスレッドを起動してビジーループさせる。 GIL の制約が無ければ、このプログラムで CPU 1 コアよりも多いリソースを消費できるはず。

import threading
import os
import sys
import time


def _busy_loop(thread_id):
    """ビジーループする関数"""
    print(f"starting thread: {thread_id}")
    while True:
        pass


def main():
    # CPU 論理コア数より 1 少ない数のスレッドを起動する
    num_threads = os.cpu_count() - 1
    for i in range(num_threads):
        thread = threading.Thread(target=_busy_loop, args=(i,))
        # 親スレッドが止まったら子スレッドも停止する
        thread.daemon = True
        # スレッドを起動する
        thread.start()

    # メインスレッドはスリープさせる
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        sys.exit(0)


if __name__ == "__main__":
    main()

フリースレッドモードが無効なビルドで実行した場合

まずは通常の、フリースレッドモードが無効なビルドから試してみよう。

uv run コマンドに --script オプションを渡して先ほどのサンプルコードを実行する。 同時に --python オプションで実行する Python 実行環境を選ぶ。 このとき、末尾に t のつかない、フリースレッドモードが無効なビルドを指定する。 gtime コマンドを先頭につけることで、プログラムが消費する CPU のリソースに関する情報を出力できる。

$ gtime uv run --script --python 3.14 busyloop.py 
starting thread: 0
starting thread: 1
starting thread: 2
starting thread: 3
starting thread: 4
starting thread: 5
starting thread: 6

実行しているのは 8 論理 CPU コアを積んだマシンなので 7 つのスレッドが走る。 その上で、リソースモニターなどを確認すると CPU 1 コア分のリソースが消費されているはず。 これは、同時に実行されるカーネルスレッドが GIL によって 1 つに制限されているため。

$ gtime uv run --script --python 3.14 busyloop.py 
starting thread: 0
starting thread: 1
starting thread: 2
starting thread: 3
starting thread: 4
starting thread: 5
starting thread: 6
^C5.06user 0.04system 0:05.14elapsed 99%CPU (0avgtext+0avgdata 15312maxresident)k
0inputs+0outputs (929major+1467minor)pagefaults 0swaps

しばらく実行して満足したら Ctrl-C でプログラムの実行を止めよう。 上記の gtime コマンドの出力にも 99%CPU という表記が確認できる。

フリースレッドモードが有効なビルドで実行した場合

次は先ほどと同じことをフリースレッドモードが有効なビルドで試す。 違いは --python の引数の末尾に t をつけるだけ。

$ gtime uv run --script --python 3.14t busyloop.py
starting thread: 0
starting thread: 1
starting thread: 2
starting thread: 3
starting thread: 4
starting thread: 5
starting thread: 6
^C29.49user 0.04system 0:04.31elapsed 684%CPU (0avgtext+0avgdata 18992maxresident)k
0inputs+0outputs (1002major+1707minor)pagefaults 0swaps

今度はビジーループする 7 つのスレッドで CPU 7 コア分のリソースが消費されるはず。 上記の gtime コマンドの出力にも 684%CPU という表記が確認できる。

まとめ

今回は uv を使ってフリースレッドモードが有効な CPython 3.14 を動作させてみた。 uv は Python の実行環境まで管理できるので、こういった場面で便利だと感じられる。

なお、フリースレッドモードを公式にサポートしたバージョンの CPython 3.14 は 2025-10-07 にリリースされたばかり。 そのため、フリースレッドモードが有効なビルドを実用する上では、サードパーティー製のパッケージ群の対応を待つ時間が必要なはず。

また、将来的には CPython でフリースレッドモードがデフォルトで有効になる「フェーズ 3」も計画されている 4。 マルチスレッドで並列処理が書けるのは本当に嬉しくて、実用的になるはずの将来が楽しみで仕方がない。