Python のフリースレッド対応

CPython 3.13 から、global interpreter lock (GIL) を無効にした free threading という Python のビルドがサポートされています。 フリースレッド実行では、利用可能な CPU コアでスレッドを並列に動かし、処理能力を十分に活用できます。 すべてのソフトウェアが自動的に恩恵を受けるわけではありませんが、スレッドの利用を考慮して設計されたプログラムは、マルチコアのハードウェア上でより速く動作します。

一部のサードパーティーパッケージ、特に extension module を含むものは、フリースレッドビルドへの対応ができておらず、GIL を再び有効にする場合があります。

このドキュメントでは、フリースレッドが Python コードに与える影響を説明します。 フリースレッドビルドに対応した C 拡張の書き方は、C API 拡張のフリースレッド対応 を参照してください。

参考

フリースレッドの Python の全体的な説明は、PEP 703 — Making the Global Interpreter Lock Optional in CPython を参照してください。

インストール

Python 3.13 から、macOS と Windows の公式インストーラでは、フリースレッド版 Python のバイナリを追加でインストールできます。 インストーラは https://www.python.org/downloads/ から入手できます。

ほかのプラットフォームについては、コミュニティが保守しているフリースレッド版 Python のインストールガイド、Installing a Free-Threaded Python を参照してください。

CPython をソースからビルドする場合、フリースレッドの Python インタープリタを作るには configure の --disable-gil オプションを使います。

フリースレッド版 Python の識別

実行中のインタープリタがフリースレッドに対応しているかは、python -VV の出力や sys.version に "free-threading build" が含まれるかで確認できます。 新しい sys._is_gil_enabled() 関数では、実行中のプロセスで GIL が実際に無効になっているかを確認できます。

sysconfig.get_config_var("Py_GIL_DISABLED") 設定変数でも、ビルドがフリースレッドに対応しているかを確認できます。 値が 1 なら対応しています。 ビルド構成に関する判断には、この方法を使うことが推奨されます。

フリースレッド版 Python のグローバルインタープリタロック

CPython のフリースレッドビルドでは、PYTHON_GIL 環境変数または -X gil コマンドラインオプションを使い、実行時に GIL を有効にすることもできます。

フリースレッド対応が明示されていない C API 拡張モジュールをインポートすると、GIL が自動的に有効になることもあります。 この場合は警告が表示されます。

個々のパッケージのドキュメントに加え、次のウェブサイトでも、よく使われるパッケージのフリースレッド対応状況を確認できます。

スレッド安全性

CPython のフリースレッドビルドは、Python レベルでは、デフォルトの GIL が有効なビルドと同様のスレッド安全性を提供することを目指しています。 dict、list、set などの組み込み型は内部ロックを使い、GIL と似た動作で並行する変更から保護します。 組み込み型が提供する保証については、スレッドセーフ性の保証 を参照してください。

注釈

可能であれば、組み込み型の内部ロックに頼る代わりに、threading.Lock などの同期プリミティブを使うことが推奨されます。

既知の制限

この節では、CPython のフリースレッドビルドの既知の制限を説明します。

不死化

フリースレッドビルドでは、一部のオブジェクトが immortal になります。 不死のオブジェクトは解放されず、参照カウントも変更されません。 これは、効率的なマルチスレッドでの性能向上を妨げる、参照カウントの競合を避けるためです。

3.14 リリース時点では、不死化されるものは次に限られます。

  • コード内の定数: 数値リテラル、文字列リテラル、ほかの定数からなるタプルリテラル。

  • sys.intern() でインターンされた文字列。

フレーム (frame) オブジェクト

別のスレッドで実行中の フレーム オブジェクトの frame.f_locals にアクセスするのは安全ではなく、インタープリタがクラッシュする可能性があります。

イテレータ (iterator)

一般に、同じイテレータオブジェクトに複数のスレッドから並行してアクセスすることは、スレッドセーフではありません。 要素が重複して取得されたり、取得できない要素が出たりする可能性があります。

シングルスレッドの性能

フリースレッドビルドは、デフォルトの GIL が有効なビルドに比べ、Python コードの実行に追加のオーバーヘッドがあります。 その大きさはワークロードとハードウェアに依存します。 pyperformance ベンチマークスイートでは、平均のオーバーヘッドは macOS aarch64 の約 1% から x86-64 Linux の約 8% までの範囲です。

動作の違い

この節では、フリースレッドビルドによる CPython の動作の違いを説明します。

コンテキスト変数

フリースレッドビルドでは、thread_inherit_context フラグはデフォルトで真です。 そのため、threading.Thread で作られたスレッドは、start() の呼び出し元の Context() のコピーを持って開始します。 デフォルトの GIL が有効なビルドでは、このフラグはデフォルトで偽なので、スレッドは空の Context() で開始します。

警告フィルタ

フリースレッドビルドでは、context_aware_warnings フラグはデフォルトで真です。 デフォルトの GIL が有効なビルドでは、偽になっています。 真の場合、warnings.catch_warnings コンテキストマネージャは、警告フィルタにコンテキスト変数を使います。 偽の場合、catch_warnings はグローバルなフィルタのリストを変更するため、スレッドセーフではありません。 詳しくは warnings モジュールを参照してください。

メモリ使用量の増加

フリースレッドビルドは、通常、デフォルトのビルドより多くのメモリを使います。 理由はいくつかあり、その多くは設計上の判断によるものです。

インターンされた文字列はすべて不死になる

Python 2.3 以降では、sys.intern() などで文字列をインターンしても、不死にはなりません。 その文字列への最後の参照がなくなると、インターン済み文字列のテーブルから削除されます。 フリースレッドビルドではこれとは異なり、インターンされた文字列はすべて不死になり、インタープリタの終了まで生存します。

GC の対象でないオブジェクトのヘッダが大きくなる

フリースレッドビルドは、異なる PyObject 構造体を使います。 デフォルトのビルドのように GC 関連の情報を PyObject 構造体の前に配置するのではなく、通常のオブジェクトヘッダの一部にしています。 例えば AMD64 プラットフォームでは、None はフリースレッドビルドで 32 バイト、デフォルトのビルドでは 16 バイトを使います。 辞書やリストなど、GC 対象のオブジェクトのサイズは両ビルドで同じです。 フリースレッドビルドでは、GC 情報のために別の領域を使わないためです。

QSBR によってメモリの解放が遅れることがある

ロックを使わないデータ構造を安全に実装するため、quiescent state-based reclamation (QSBR) という安全なメモリ回収 (SMR) の方式を使います。 ロックなしのアクセスを許すデータ構造の領域には QSBR が使われ、メモリはすぐには解放されず、解放が遅延されます。 例として、リストオブジェクトや辞書のキーオブジェクトがあります。 QSBR の実装の詳細は、CPython ソースツリーの InternalDocs/qsbr.md を参照してください。 gc.collect() を実行すると、QSBR によって保持されているメモリはすべて解放されるはずです。 ただし、QSBR がメモリを解放しても、下位のメモリアロケータがすぐに OS へ返すとは限らず、プロセスの常駐メモリ量 (RSS) が減らない場合があります。

mimalloc と pymalloc の比較

デフォルトのビルドは通常、512 バイト以下の小さな割り当てに pymalloc メモリアロケータを使います。 フリースレッドビルドは pymalloc を使わず、すべての Python オブジェクトを mimalloc で割り当てます。 pymalloc には、割り当てブロックごとのオーバーヘッドが小さい、メモリの断片化を効果的に防ぐ、未使用メモリを速やかに OS へ返すといった、メモリ使用量を抑える特性があります。 mimalloc もこれらの点で優れていますが、オーバーヘッドがやや大きい場合があります。

フリースレッドビルドでは、mimalloc は複数の独立したヒープ (現在は 4 つ) でメモリを管理します。 例えば、GC に対応するオブジェクトはすべて専用のヒープから割り当てられます。 ヒープを分けると、あるヒープの空きメモリを別のヒープからの割り当てに使うことはできません。 また、一部のヒープでは、ヒープの基盤となるメモリ領域 (mimalloc の用語で「ページ」) の解放に QSBR (quiescent-state based reclamation) を使います。 そのため、ページ内の全メモリブロックが解放されてから、そのページが新しい割り当てに使えるようになるか OS へ返されるまでに、遅延が生じます。

mimalloc も、解放されたメモリを OS に返すまでに遅延を設けています。 MIMALLOC_PURGE_DELAY 環境変数を 0 に設定すると、この遅延を短くできます。 ただし、アロケータの性能が低下する可能性が高いことに注意してください。

フリースレッドの参照カウントではオブジェクトの寿命が延びることがある

デフォルトのビルドでは、通常、参照カウントがゼロになるとオブジェクトが解放されます。 フリースレッドビルドは「バイアス参照カウント」を使い、現在のスレッドが「所有する」オブジェクトには高速な処理経路、それ以外には低速な経路を使います。 詳しくは PEP 703 を参照してください。 参照カウントが queued (キュー待ち) 状態になると、解放が遅延することがあります。 キュー待ち状態は、バイトコード評価器の eval breaker 部分で解除されます。

フリースレッドビルドでは、「遅延参照カウント」という別の方式も使えます。 これはオブジェクトごとにフラグを設定して有効にします。 次の型で遅延参照カウントが有効になっています。

  • モジュールオブジェクト

  • モジュールのトップレベルの関数

  • クラススコープで定義されたメソッド

  • デスクリプタオブジェクト

  • threading.local で作られたスレッドローカルオブジェクト

遅延参照カウントを有効にすると、Python 関数のスタックからの参照は参照カウントに加算されません。 この方式は、特に複数のスレッドから使われるオブジェクトについて、参照カウントのオーバーヘッドを減らします。 スタックからの参照を数えないため、内部の参照カウントがゼロになっても、オブジェクトはすぐには解放されません。 代わりに、次回の GC で調べられ、スタックからの参照がなければ解放されます。 つまり、通常のように参照カウントがゼロになった時点ではなく、GC によって解放されます。

スレッドごとの参照カウントによってオブジェクトの解放が遅れることがある

頻繁に共有されるオブジェクトの参照カウントフィールドでの競合を避けるため、フリースレッドビルドは、選ばれた少数の型に「スレッドごとの参照カウント」も使います。 単一の共有参照カウントを更新する代わりに、各スレッドがローカルな参照カウントの配列を持ち、オブジェクトに割り当てられた一意の ID をインデックスとして使います。 実際の参照カウントを計算するのは、オブジェクトのローカルカウントがゼロになったときだけで、その際に各スレッドのカウントを合計します。 現在、この方式を使うのは次のものです。

  • ヒープ型オブジェクト (Python で作成したクラス)

  • コードオブジェクト

  • モジュールオブジェクトの __dict__

スレッドごとのカウントをオブジェクトに集約してからでなければ解放できないため、この方式を使うオブジェクトは、通常、デフォルトのビルドより遅く解放されます。 特に、そのオブジェクトを参照したスレッドが、バイトコード評価器の eval breaker などの安全な地点に到達するか、終了するまでは、通常は解放されません。 gc.collect() を実行すると、スレッドごとのカウントが集約され、これらのオブジェクトを解放できるようになります。