C API 拡張のフリースレッド対応

CPython 3.13 から、global interpreter lock (GIL) を無効にして実行する free threading という構成がサポートされています。 このドキュメントでは、C API を使う拡張をフリースレッドに対応させる方法を説明します。

C からフリースレッドビルドを識別する

CPython の C API は Py_GIL_DISABLED マクロを公開しています。 フリースレッドビルドでは 1 に定義され、通常のビルドでは定義されません。 これを使って、フリースレッドビルドでだけ実行するコードを有効にできます。

#ifdef Py_GIL_DISABLED
/* フリースレッドビルドでだけ実行するコード */
#endif

注釈

Windows ではこのマクロは自動的には定義されないため、ビルド時にコンパイラへ指定する必要があります。 実行中のインタープリタでこのマクロが定義されていたかは、sysconfig.get_config_var() 関数で確認できます。

モジュールの初期化

拡張モジュールは、GIL を無効にした実行に対応していることを明示する必要があります。 そうしなければ、拡張のインポート時に警告が出され、実行時に GIL が有効になります。

GIL を無効にした実行に対応していることを示す方法は、拡張が複数フェーズ初期化と単一フェーズ初期化のどちらを使うかによって、2 通りあります。

複数フェーズ初期化

複数フェーズ初期化 (PyModuleDef_Init()、PyModExport_* エクスポートフック、PyModule_FromSlotsAndSpec() など) を使う拡張では、モジュール定義に Py_mod_gil スロットを追加してください。 古い CPython バージョンにも対応する場合は、PY_VERSION_HEX を確認し、対応するバージョンでだけこのスロットを追加してください。

static struct PyModuleDef_Slot module_slots[] = {
    ...
#if PY_VERSION_HEX >= 0x030D0000
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
    {0, NULL}
};

単一フェーズ初期化

従来の 単一フェーズ初期化 (つまり PyModule_Create()) を使う拡張では、PyUnstable_Module_SetGIL() を呼び出して、GIL を無効にした実行に対応していることを示してください。 この関数はフリースレッドビルドでのみ定義されるため、通常のビルドでのコンパイルエラーを避けるには、呼び出しを #ifdef Py_GIL_DISABLED で囲んでください。

static struct PyModuleDef moduledef = {
    PyModuleDef_HEAD_INIT,
    ...
};

PyMODINIT_FUNC
PyInit_mymodule(void)
{
    PyObject *m = PyModule_Create(&moduledef);
    if (m == NULL) {
        return NULL;
    }
#ifdef Py_GIL_DISABLED
    PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
    return m;
}

API 全般の指針

C API の大部分はスレッドセーフですが、例外もあります。

  • 構造体のフィールド: Python C API のオブジェクトや構造体のフィールドが並行して変更される可能性がある場合、そのフィールドへの直接アクセスはスレッドセーフではありません。

  • マクロ: PyList_GET_ITEM、PyList_SET_ITEM などのアクセス用マクロや、PySequence_Fast() が返すオブジェクトを使う PySequence_Fast_GET_SIZE などのマクロは、エラーチェックもロックも行いません。 コンテナオブジェクトが並行して変更される可能性がある場合、これらのマクロはスレッドセーフではありません。

  • 借用参照: 借用参照 を返す C API 関数は、参照先を保持するオブジェクトが並行して変更される場合、スレッドセーフでないことがあります。 詳しくは 借用参照の節 を参照してください。

コンテナのスレッド安全性

PyListObject、PyDictObject、PySetObject などのコンテナは、フリースレッドビルドでは内部でロックを使います。 例えば、PyList_Append() は要素を追加する前にリストをロックします。

PyDict_Next

注意すべき例外は PyDict_Next() で、この関数は辞書をロックしません。 辞書が並行して変更される可能性がある場合は、Py_BEGIN_CRITICAL_SECTION を使い、イテレート中の辞書を保護してください。

Py_BEGIN_CRITICAL_SECTION(dict);
PyObject *key, *value;
Py_ssize_t pos = 0;
while (PyDict_Next(dict, &pos, &key, &value)) {
    ...
}
Py_END_CRITICAL_SECTION();

借用参照

一部の C API 関数は 借用参照 を返します。 参照先を保持するオブジェクトが並行して変更される場合、これらの API はスレッドセーフではありません。 例えば、リストが並行して変更される可能性がある状況で PyList_GetItem() を使うのは安全ではありません。

次の表は、借用参照を返す API の一部と、代わりに使える 強参照 を返す API を示します。

借用参照を返す API

強参照を返す API

PyList_GetItem()

PyList_GetItemRef()

PyList_GET_ITEM()

PyList_GetItemRef()

PyDict_GetItem()

PyDict_GetItemRef()

PyDict_GetItemWithError()

PyDict_GetItemRef()

PyDict_GetItemString()

PyDict_GetItemStringRef()

PyDict_SetDefault()

PyDict_SetDefaultRef()

PyDict_Next()

なし (PyDict_Next を参照)

PyWeakref_GetObject()

PyWeakref_GetRef()

PyWeakref_GET_OBJECT()

PyWeakref_GetRef()

PyImport_AddModule()

PyImport_AddModuleRef()

PyCell_GET()

PyCell_Get()

借用参照を返すすべての API が問題になるわけではありません。 例えば、タプルはイミュータブルなので PyTuple_GetItem() は安全です。 また、上記 API の使い方がすべて問題になるわけでもありません。 例えば、PyDict_GetItem() は関数呼び出しのキーワード引数辞書の解析によく使われます。 この辞書は実質的にプライベートで、ほかのスレッドからアクセスできないため、その状況で借用参照を使うのは安全です。

これらの関数の一部は Python 3.13 で追加されました。 古い Python バージョン向けには、pythoncapi-compat パッケージでこれらの関数の実装を利用できます。

メモリ割り当て API

Python のメモリ管理 C API は、raw、mem、object という 3 つの 割り当てドメイン の関数を提供します。 フリースレッドビルドではスレッド安全性のため、object ドメインで割り当てるのは Python オブジェクトだけでなければならず、すべての Python オブジェクトをこのドメインで割り当てる必要があります。 以前の Python では、これは推奨事項にすぎず、必須条件ではありませんでした。

注釈

拡張内の PyObject_Malloc() の使用箇所を探し、割り当てたメモリを Python オブジェクトに使っているか確認してください。 バッファの割り当てには、PyObject_Malloc() ではなく PyMem_Malloc() を使ってください。

スレッド状態と GIL の API

Python は、スレッド状態と GIL を管理するために、次のような関数やマクロを提供しています。

フリースレッドビルドで GIL が無効な場合も、スレッド状態の管理にはこれらの関数を引き続き使う必要があります。 例えば、Python の外部で作成したスレッドから Python API を呼び出す場合は、先に PyThreadState_Ensure() を呼び出し、有効な Python スレッド状態を持つことを保証しなければなりません。

I/O やロック取得などのブロッキング操作の前後では、引き続き PyEval_SaveThread() や Py_BEGIN_ALLOW_THREADS を使ってください。 これにより、ほかのスレッドが 循環ガベージコレクタ を実行できるようになります。

拡張の内部状態の保護

拡張には、以前は GIL によって保護されていた内部状態があるかもしれません。 その状態を保護するためにロックを追加する必要がある場合があります。 方法は拡張によりますが、よくあるパターンには次のものがあります。

  • キャッシュ: グローバルなキャッシュは、共有状態が生まれる典型的な場所です。 ロックでキャッシュを保護するか、性能上不可欠でなければ、フリースレッドビルドではキャッシュを無効にすることを検討してください。

  • グローバルな状態: グローバルな状態には、ロックによる保護やスレッドローカルストレージへの移動が必要な場合があります。 C11 と C++11 は、スレッドローカルストレージ のために thread_local または _Thread_local を提供しています。

クリティカルセクション

CPython のフリースレッドビルドには、通常なら GIL が保護するデータを保護する「クリティカルセクション」という仕組みがあります。 拡張の開発者がその内部実装を直接扱うことはなくても、特定の C API 関数を使うときや共有状態を管理するときには、動作を理解しておくことが重要です。

クリティカルセクションとは

概念的には、クリティカルセクションは単純なミューテックスの上に設けられた、デッドロックを回避するための層です。 各スレッドは、有効なクリティカルセクションのスタックを保持します。 スレッドがクリティカルセクションのロックを取得する必要があると、その基盤となるミューテックスの取得を試みます。 これは、PyDict_SetItem() のようなスレッドセーフな C API 関数の呼び出しで暗黙に行われる場合も、マクロで明示的に行われる場合もあります。

クリティカルセクションの使い方

クリティカルセクションを使う主な API は次のとおりです。

これらのマクロは新しいローカルスコープを作るため、必ず対応する組で使い、同じ C のスコープに置く必要があります。 非フリースレッドビルドでは何もしないので、両方のビルドに対応するコードにも安全に追加できます。

クリティカルセクションのよくある用途は、オブジェクトの内部属性へのアクセス中に、そのオブジェクトをロックすることです。 例えば、拡張型に内部的なカウントのフィールドがあれば、その読み書きにクリティカルセクションを使えます。

// read the count, returns new reference to internal count value
PyObject *result;
Py_BEGIN_CRITICAL_SECTION(obj);
result = Py_NewRef(obj->count);
Py_END_CRITICAL_SECTION();
return result;

// write the count, consumes reference from new_count
Py_BEGIN_CRITICAL_SECTION(obj);
obj->count = new_count;
Py_END_CRITICAL_SECTION();

クリティカルセクションの仕組み

通常のロックとは異なり、クリティカルセクションはその期間全体にわたる排他的アクセスを保証しません。 クリティカルセクション内で別のロックの取得や I/O などによってスレッドがブロックする場合、クリティカルセクションは一時的に中断され、すべてのロックが解放されます。 ブロッキング操作が完了すると再開されます。

これは、スレッドがブロッキング呼び出しを行うときの GIL の動作に似ています。 主な違いは次のとおりです。

  • クリティカルセクションは全体で 1 つではなく、オブジェクト単位で動作します。

  • クリティカルセクションは各スレッド内でスタックの順序に従います。 開始マクロと終了マクロは同じスコープ内で対にする必要があるため、この順序が強制されます。

  • クリティカルセクションは、ブロックする可能性のある操作の前後で、自動的にロックを解放して再取得します。

デッドロックの回避

クリティカルセクションは、次の 2 つの方法でデッドロックの回避を助けます。

  1. スレッドが別のスレッドに保持されているロックを取得しようとすると、まず自身の有効なクリティカルセクションをすべて中断し、それらのロックを一時的に解放します。

  2. ブロッキング操作が完了すると、最初に最上位のクリティカルセクションだけが再取得されます。

このため、複数のオブジェクトを同時にロックする目的で、クリティカルセクションの入れ子を使うことはできません。 内側のクリティカルセクションが外側を中断する可能性があるためです。 2 つのオブジェクトを同時にロックするには、代わりに Py_BEGIN_CRITICAL_SECTION2 を使ってください。

ここで説明したロックは、PyMutex に基づくロックだけです。 クリティカルセクションの実装は、POSIX ミューテックスなどのほかのロック機構を認識せず、影響も与えません。 また、どの PyMutex でブロックしてもクリティカルセクションは中断されますが、解放されるのはクリティカルセクションの一部であるミューテックスだけです。 クリティカルセクションを使わずに取得した PyMutex は解放されないため、同じデッドロック回避の効果は得られません。

重要な注意点

  • クリティカルセクションは一時的にロックを解放することがあるため、ほかのスレッドが保護対象のデータを変更できます。 ブロックする可能性のある操作の後で、データの状態を仮定する際には注意してください。

  • ロックが一時的に解放 (中断) される可能性があるため、クリティカルセクションに入っても、その期間全体にわたって保護対象のリソースへ排他的にアクセスできるとは限りません。 クリティカルセクション内で、別のロックの取得やブロッキング I/O などを行う関数を呼び、その処理がブロックすると、そのスレッドがクリティカルセクションを通じて保持するロックはすべて解放されます。 これは、ブロッキング呼び出し中に GIL が解放されることがあるのと同様です。

  • ある時点で保持が保証されるのは、最後に入った最上位のクリティカルセクションのロックだけです。 入れ子になった外側のクリティカルセクションのロックは、中断されている可能性があります。

  • これらの API で同時にロックできるオブジェクトは最大 2 つです。 それ以上をロックする必要があれば、コードの構成を見直す必要があります。

  • クリティカルセクションで同じオブジェクトを 2 回ロックしようとしてもデッドロックにはなりませんが、この用途に特化した再入可能ロックほど効率的ではありません。

  • Py_BEGIN_CRITICAL_SECTION2 では、実装がデッドロックを回避するため、オブジェクトの順序は正しさに影響しません。 ただし、常に一貫した順序でロックするのがよい習慣です。

  • クリティカルセクションのマクロは主に、上述のようなデッドロックが起こり得る CPython 内部の操作に関与する Python オブジェクト へのアクセスを保護するものです。 拡張だけが使う内部状態の保護には、標準のミューテックスなどの同期プリミティブのほうが適している場合があります。

オブジェクトごとのロック (ob_mutex)

フリースレッドビルドでは、各 Python オブジェクトに PyMutex 型の ob_mutex フィールドがあります。 このミューテックスは クリティカルセクション API 専用 です (Py_BEGIN_CRITICAL_SECTION / Py_END_CRITICAL_SECTION)。

警告

PyMutex_Lock(&obj->ob_mutex) で ob_mutex を直接ロック しないでください。 同じミューテックスに対して、PyMutex_Lock の直接呼び出しとクリティカルセクション API を混在させると、デッドロックが起こる可能性があります。

自分のコードでは特定の型のオブジェクトにクリティカルセクションを使っていなくても、CPython 内部ではどの Python オブジェクトにもクリティカルセクション API を使う可能性があります。

拡張型に独自のロックが必要な場合は、オブジェクトの構造体に別の PyMutex フィールド、またはほかの同期プリミティブを追加してください。 PyMutex は非常に軽量なので、追加で 1 つ持ってもコストはほぼ無視できます。

フリースレッドビルド向けの拡張のビルド

C API extensions need to be built specifically for the free-threaded build. The wheels, shared libraries, and binaries are indicated by a t suffix.

  • pypa/manylinux は、python3.14t のように t サフィックスを付けたフリースレッドビルドに対応しています。

  • pypa/cibuildwheel は、Python 3.14 以降のフリースレッドビルド向け wheel の作成に対応しています。

Limited C API と Stable ABI

The free-threaded build does not currently support the Limited C API or the stable ABI. If you use setuptools to build your extension and currently set py_limited_api=True you can use py_limited_api=not sysconfig.get_config_var("Py_GIL_DISABLED") to opt out of the limited API when building with the free-threaded build.

注釈

You will need to build separate wheels specifically for the free-threaded build. If you currently use the stable ABI, you can continue to build a single wheel for multiple non-free-threaded Python versions.

Windows

公式 Windows インストーラの制限により、ソースから拡張をビルドする際は、Py_GIL_DISABLED=1 を手動で定義する必要があります。

参考

Porting Extension Modules to Support Free-Threading: 拡張の開発者向けにコミュニティが保守している移植ガイド。