Python Monthly Topics

PythonのGILと3.13の実験的な新機能「free threading」知る

福田@JunyaFffです。今月の「Python Monthly Topics」は、Python 3.13の新機能「free threading」について解説します。

はじめに

2024年10月にリリースされたPython 3.13。その中でもっとも注目すべき実験的な新機能の「free threading」について紹介します。本記事ではfree threadingについて紹介するにあたり、避けては通れない「Global Interpreter Lock(以下GIL⁠⁠」というCPythonのロック機構の基本を説明して、free threadingについての概要と動作検証した結果を紹介します。

Python 3.13での他の新機能については先月の記事Python 3.13で更新された機能の紹介をご参照ください。

なお、今回の記事を書くにあたり参考にしたドキュメントは下記になります。

GILとは

GILの基本概念

GILとは、複数のスレッドが同時にメモリにアクセスすることがないように、複数のスレッドでPythonインタープリターを同時に実行することを防ぐためのものです。簡単にいうと、Pythonプログラムは複数のスレッドを作成できるものの、一度に実際のPythonコードを実行できるのは1つのスレッドだけになります。

Pythonの並列処理で多くの方が制約として認識しているのが、このGILというロック機構です。

GILの利点

このGILというロック機構には、いくつかの大きな利点があります。

  1. メモリ管理の簡素化:Pythonのガベージコレクションとメモリ管理を簡単かつ安全に実装
  2. シングルスレッドパフォーマンスの最適化:複雑なロック機構を排除することでシングルスレッドの実行速度を向上
  3. C拡張モジュールとの互換性:C拡張モジュールがスレッドセーフではないことを考慮するため、GILによって安全性を確保

GILがもたらす制約

その反面、GILがあることによって、Pythonの並列処理では以下のような制約が生じていました。

  1. 真の並列処理ができない:マルチコアCPUがあたりまえの現代において、Pythonは複数のコアを効率的に活用できない
  2. CPU負荷の高い処理が遅い:計算集約型のタスクを並列化してもほとんど恩恵が得られない
  3. 並列処理のためには複雑な回避策が必要:マルチプロセスによる並列化や、C拡張を使った実装を行う必要がある

これらの制約を解消するため、Python開発チームは長年にわたってGILの撤廃や改良を検討してきました。そして、Python 3.13でついにfree threadingという機能が実験的に導入されました。

free threadingとは

free threadingの技術的概要

3.13のfree threadingは、CPythonからGILを完全に排除するのではなく、オプションとして無効化できる設計になっています。これにより、後方互換性を維持しつつ、並列処理のメリットを享受できるようになるための実験的な機能です。

技術的な特徴は以下の通りです。

  1. より細かい粒度のロック機構[1]:GILの代わりに、必要なオブジェクトごとに細かいロック機構を導入
  2. 並行ガベージコレクション:複数のスレッドでPythonプログラムが同時に実行されても安全にメモリ管理ができるガベージコレクション
  3. アトミック参照カウント:オブジェクトの参照カウントを原子的に操作することで、オブジェクトの整合性を保ちながらスレッド間の競合を回避
  4. バイアスロック:頻繁にアクセスされるオブジェクトに対して、特定のスレッドが優先的にロックを獲得できる最適化

全体のロックに代わって細かいロック機構を採用することで、スレッド間の競合を最小限に抑えつつ、並列処理の実現が可能になります。

詳細はPEP 703 – Making the Global Interpreter Lock Optional in CPythonを参照してください。

Python 3.13 free threading のインストール方法

では実際にfree threadingを体験してみましょう。以下の方法で利用が可能です。

方法1⁠公式ビルドをダウンロード

Python.orgからビルド済みのインストーラーをダウンロードし、オプションとなっている「Free-threaded Python [experimental]」を有効にし、インストールできます。

  1. Python Downloadsから最新のPython 3.13をダウンロード
  2. インストーラーを実行してインストール
  3. customizeを選択
インストール画面 - customize
インストール画面 - customize
  1. Free-threaded Python [experimental] を選択しインストールを行う
インストール画面 - Free-threaded Python
インストール画面 - Free-threaded Python

python3.13t(Windowsであればpython3.13t.exe)がインストールされます。3.13tがfree threading版のPythonです。

方法2⁠uvを使用する

過去の記事2024年9月: さらなる進化を遂げた「uv」の新機能でも紹介した「uv」を使用して、free threading版のPythonをインストールすることもできます。

$ uv python install 3.13t
Installed Python 3.13.2 in 4.62s
 + cpython-3.13.2+freethreaded-macos-aarch64-none

また、uv runコマンドでもPythonスクリプトを実行できます。

$ uv run --python 3.13t sample.py
 

なお、uvのインストールについてはこちらをご参照ください。

方法3⁠ソースコードからビルド

ビルドオプション--disable-gilを有効にし、ソースコードからビルドすることも可能です。

関連ライブラリの更新/インストール
$ sudo apt update
$ sudo apt install build-essential zlib1g-dev \
> libncurses5-dev libgdbm-dev libnss3-dev libssl-dev \
> libreadline-dev libffi-dev curl clang
Python 3.13.2をダウンロード
$ curl -O https://www.python.org/ftp/python/3.13.2/Python-3.13.2.tgz
$ tar zxvf Python-3.13.2.tgz
$ cd Python-3.13.2
$ ./configure --disable-gil
$ make
$ make install

他のコンパイルオプションについては、適宜変更してください。筆者の環境ではよく利用されるオプション、--enable-optimizations--with-ltoを指定してビルドできることを確認しました。

動作確認

筆者の環境では、公式よりダウンロードしインストールしたものを利用します。インストーラーを利用した場合には、python3.13tがfree-threading版のPythonです。

$ python3.13t
Python 3.13.2 experimental free-threading build (main, Feb 12 2025, 15:02:03) [Clang 19.1.6 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

Python 3.13のfree threadingでの動作検証

実際にfree threadingを使った並列処理のコード例を見ていきます。

動作環境

動作環境は以下のとおりです。

  • Apple M2
  • Sequoia 15.3.2
  • Python 3.13.2 / Python 3.13.2t

CPUバウンドな処理をマルチスレッドで実行してみる

Pythonのスレッドをconcurrent.futuresThreadPoolExecutorを使って実行します。実行するサンプルコードは以下のとおりです。

1億回繰り返す計算処理をスレッド4つで実行しています。

example_thread.py:スレッドを試すシンプルな計算スクリプト
import concurrent.futures
import time

def cpu_bound_task() -> int:
    result = 0
    for i in range(10**8):  # 1億回繰り返す
        result += i % 10
    return result

def main():
    start = time.time()
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
        for f in [executor.submit(cpu_bound_task) for _ in range(4)]:
            f.result()
    end = time.time()
    print(f"Time taken: {end - start:.2f} seconds")

if __name__ == "__main__":
    main()    

上記のコードをPython 3.13で実行した結果は以下の通りです。

$ python3.13 example_thread.py
Time taken: 11.51 seconds

次に、free threading版で実行してみましょう。高速化されているのがわかります。3.13では1スレッドずつしか動作しないため遅いですが、free threading版では4スレッドが同時に動くため、速くなっていることがわかります。

$ python3.13t example_thread.py
Time taken: 3.57 seconds

CPUバウンドな処理をマルチプロセスで実行してみる

さて、同じ処理をマルチプロセスを使って実行してみます。先ほどのコードのThreadPoolExecutorProcessPoolExecutorに変えるだけで、マルチスレッドをマルチプロセスに変えて実行できます。

example_process.py:マルチプロセスを試すシンプルな計算スクリプト
import concurrent.futures
import time

def cpu_bound_task() -> int:
    result = 0
    for i in range(10**8):  # 1億回繰り返す
        result += i % 10
    return result

def main():
    start = time.time()
    with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
        for f in [executor.submit(cpu_bound_task) for _ in range(4)]:
            f.result()
    end = time.time()
    print(f"Time taken: {end - start:.2f} seconds")

if __name__ == "__main__":
    main()    

マルチスレッドの例と同様に、3.13と3.13tでそれぞれ実行してみます。結果に大きな差はありませんでした。前述のとおり、free threadingはGILを排除することが目的であり、マルチプロセスはGILの影響を受けないため、マルチプロセスを利用する場合にはfree threadingの効果が現れにくいことがわかりました。

$ python3.13 example_process.py
Time taken: 3.42 seconds
$ python3.13t example_process.py
Time taken: 3.57 seconds

さて、続いて想定どおり結果に差が出たマルチスレッドで、実行する処理を変えて試してみましょう。先ほどのexample_thread.pycpu_bound_task関数を変えて、どのように変化するか確認してみます。

試したパターン(すべて ThreadPoolExecutor + 4並列)

  1. 座標を生成して距離を計算する
example_thread_1_point.py
def cpu_bound_task():
    """座標を生成して距離を計算する"""
    points = [(x, x + 1) for x in range(1_000_000)]  # 100万個の座標を生成
    total = 0.0
    for x, y in points:
        total += (x ** 2 + y ** 2) ** 0.5
    return total
  1. 辞書アクセスを含む計算
example_thread_2_dict.py
def cpu_bound_task():
    """辞書アクセスを含む計算"""
    items = [{"x": i, "y": i + 1} for i in range(1_000_000)]  # 100万個の辞書を生成
    total = 0.0
    for item in items:
        total += (item["x"] ** 2 + item["y"] ** 2) ** 0.5
    return total
  1. 文字列生成を含む処理
example_thread_3_string.py
def cpu_bound_task():
    """文字列生成を含むCPUバウンドなタスク"""
    total = 0
    for i in range(10_000_000):  # 1000万回繰り返す
        # 文字列を生成して長さを計算
        s = f"item-{i}"
        total += len(s)
    return total

それぞれ実行してみたところ、以下のような結果になりました。生成処理やdictやlistへのアクセスを含む処理の場合、free threading版のほうが遅い結果となりました。

処理内容 ファイル名 3.13(GILあり) 3.13t(Free Threading)
座標生成+距離計算 example_thread_1_point.py 0.75 秒 24.51 秒
辞書のリスト生成+処理 example_thread_2_dict.py 0.92 秒 25.37 秒
f-string + len()(文字列) example_thread_3_string.py 3.64 秒 3.95 秒

なぜ遅い場合があるのか

free threading版のPython 3.13では既知の課題があります。

  • シングルスレッドの性能低下:free threadingを有効にすると、シングルスレッドの実行時に約40%の性能低下が見られる
  • メモリ使用量の増加:細粒度のロック機構やImmortal objects[2]などの実装により、メモリ使用量が増加

シングルスレッドでの性能低下を確認するため、以下のコードの実行時間を比較してみましょう。

example_dict.py
import time

start = time.time()
items = [{"x": i, "y": i + 1} for i in range(1_000_000)]  # 100万個の辞書を生成
end = time.time()
print(f"Time taken: {end - start:.2f} seconds")

試してみたところ、既知の課題どおり、free threadingが遅いことがわかりました。

$ python3.13 example_dict.py
Time taken: 0.08 seconds

$ python3.13t example_dict.py
Time taken: 2.21 seconds

今後の改善の方向性と新しい Python 3.14での対応

このような課題に対して、今後のPythonでは以下のような改善が予定されています。

  • シングルスレッド性能の向上:現在のパフォーマンス低下は主にPEP 659: specializing adaptive interpreterが無効になっていることが原因。Python 3.14ではthread-safeな方法でこれを再有効化する予定
  • 既知の制限の解消:Immortal objectsによるメモリ使用量の増加などの既知の制限事項は、今後のリリースで対処されていく予定

今後の予定や改善点の詳細については、以下のリソースを参照してください。

2025年3月時点での最新のアルファ版である、3.14.0 alpha 6が3月14日にリリースされているため、試してみましょう。注意事項として、3.14はまだアルファ版であり、安定性やパフォーマンスが保証されていないため、実行結果は参考程度にしてください[3]

先ほどと同じ3種類のスクリプトをPython 3.140a6で実行した結果が下記になります。1つ目のスクリプトは3.14/3.14tのどちらも遅くなってしまいましたが、2つ目、3つ目のスクリプトは3.14tで大幅に改善が見られています。

処理内容 ファイル名 3.13(GILあり) 3.13t(Free Threading) 3.14(GILあり) 3.14t(Free Threading)
座標生成+距離計算 example_thread_1_point.py 0.75秒 24.51秒 5.19秒 21.32秒
辞書のリスト生成+処理 example_thread_2_dict.py 0.92秒 25.37秒 1.40秒 0.64秒 ✅
f-string + len()(文字列) example_thread_3_string.py 3.64秒 3.95秒 3.16秒 0.83秒 ✅

また、今回の記事では触れていませんが、free threading版では、同じスレッドで作成されたオブジェクトに対してはロックが最小限になるように最適化されています。そのため、別スレッドで対応したオブジェクトを更新しようとすると、ロックが発生し、処理が遅くなることがあります。詳細についてはPython 3.13の新機能(その1⁠⁠ PEP 703: フリースレッドモードも合わせてご参照ください。

free threading対応状況

free threadingは実験的な機能であり、すべてのライブラリが対応しているわけではありません。とくにC拡張モジュールや、GILに依存しているライブラリは注意が必要です。free threadingに対応しているかどうか、以下のリソースを活用して確認してみましょう。

まとめ

Python 3.13で追加されたfree threading。この機能により、マルチコアCPUの能力を活かした真の並列処理が今後可能になります。

本記事で見てきたように、CPUバウンドな処理においてfree threadingを有効にすることで、従来のマルチスレッド処理が高速化されることを確認できました。これにより、Pythonでの並列処理の選択肢が広がり、特定のユースケースではマルチプロセスを使わずにマルチスレッドで高いパフォーマンスを得られるようになります。

ただし、実験的な機能であり課題も残っているため、プロダクション環境での採用は慎重に検討する必要があります。また、利用するライブラリのfree threading対応状況も確認しておくことも重要です。

PEPでは、段階的な移行を目指していると書かれています。長期的には、ビルド時オプションからランタイム制御へと移行し、最終的にはGILがデフォルトで無効になることが期待されています。今後のPythonのリリースでfree threadingがさらに改良され、最終的にはGILのない状態がデフォルトになる日も来るかもしれません。今後もキャッチアップしていきましょう。

おすすめ記事

記事・ニュース一覧