C API 擴充套件對自由執行緒的支援

從 3.13 版本開始,CPython 實驗性地支援在停用全域性直譯器鎖 (GIL) 的配置下執行,該配置稱為自由執行緒。本文件介紹瞭如何調整 C API 擴充套件以支援自由執行緒。

在 C 中識別自由執行緒構建

CPython C API 公開了 Py_GIL_DISABLED 宏:在自由執行緒構建中,它被定義為 1,而在常規構建中則未定義。你可以使用它來啟用僅在自由執行緒構建下執行的程式碼

#ifdef Py_GIL_DISABLED
/* code that only runs in the free-threaded build */
#endif

模組初始化

擴充套件模組需要明確表明它們支援在停用 GIL 的情況下執行;否則,匯入擴充套件程式將引發警告並在執行時啟用 GIL。

有兩種方法可以指示擴充套件模組是否支援在停用 GIL 的情況下執行,具體取決於擴充套件程式是使用多階段初始化還是單階段初始化。

多階段初始化

使用多階段初始化的擴充套件(即,PyModuleDef_Init())應該在模組定義中新增一個 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}
};

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

單階段初始化

使用單階段初始化的擴充套件(即,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_ITEMPyList_SET_ITEM 之類的訪問宏不執行任何錯誤檢查或鎖定。如果容器物件可能會被併發修改,則這些宏不是執行緒安全的。

  • 借用引用:如果包含物件被併發修改,則返回借用引用的 C API 函式可能不是執行緒安全的。有關更多資訊,請參閱有關借用引用的部分。

容器執行緒安全

諸如 PyListObjectPyDictObjectPySetObject 之類的容器在自由執行緒構建中執行內部鎖定。例如,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

PyList_GetItem()

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()

並非所有返回借用引用的 API 都有問題。例如,PyTuple_GetItem() 是安全的,因為元組是不可變的。同樣,並非所有上述 API 的使用都有問題。例如,PyDict_GetItem() 通常用於解析函式呼叫中的關鍵字引數字典;那些關鍵字引數字典實際上是私有的(其他執行緒無法訪問),因此在該上下文中使用借用引用是安全的。

其中一些函式是在 Python 3.13 中新增的。你可以使用 pythoncapi-compat 包為較舊的 Python 版本提供這些函式的實現。

記憶體分配 API

Python 的記憶體管理 C API 在三個不同的分配域中提供函式:“raw”、“mem” 和 “object”。為了執行緒安全,自由執行緒構建要求僅使用 object 域分配 Python 物件,並且所有 Python 物件都使用該域分配。這與之前的 Python 版本不同,在之前的版本中,這只是一種最佳實踐,而不是硬性要求。

注意

在你的擴充套件中搜索 PyObject_Malloc() 的用法,並檢查分配的記憶體是否用於 Python 物件。使用 PyMem_Malloc() 分配緩衝區,而不是 PyObject_Malloc()

執行緒狀態和 GIL API

Python 提供了一組用於管理執行緒狀態和 GIL 的函式和宏,例如

即使GIL 被停用,這些函式仍應在自由執行緒構建中使用以管理執行緒狀態。例如,如果你在 Python 之外建立執行緒,則必須在呼叫 Python API 之前呼叫 PyGILState_Ensure(),以確保執行緒具有有效的 Python 執行緒狀態。

你應該繼續在阻塞操作(例如 I/O 或鎖獲取)周圍呼叫 PyEval_SaveThread()Py_BEGIN_ALLOW_THREADS,以允許其他執行緒執行迴圈垃圾收集器

保護內部擴充套件狀態

你的擴充套件可能具有以前受 GIL 保護的內部狀態。你可能需要新增鎖定來保護此狀態。該方法將取決於你的擴充套件,但一些常見的模式包括

  • 快取:全域性快取是共享狀態的常見來源。考慮使用鎖來保護快取,或者如果快取對效能不是至關重要的,則在自由執行緒構建中停用快取。

  • 全域性狀態:全域性狀態可能需要用鎖保護,或者移動到執行緒區域性儲存。C11 和 C++11 提供了 thread_local_Thread_local 用於執行緒區域性儲存

為自由執行緒構建構建擴充套件

C API 擴充套件需要專門為自由執行緒構建構建。wheels、共享庫和二進位制檔案用 t 字尾表示。

有限 C API 和穩定 ABI

自由執行緒構建目前不支援有限 C API 或穩定 ABI。如果您使用 setuptools 構建擴充套件,並且當前設定了 py_limited_api=True,您可以使用 py_limited_api=not sysconfig.get_config_var("Py_GIL_DISABLED") 在使用自由執行緒構建時選擇退出有限 API。

注意

您將需要專門為自由執行緒構建構建單獨的 wheels。如果您當前使用穩定 ABI,您可以繼續為多個非自由執行緒 Python 版本構建單個 wheel。

Windows

由於官方 Windows 安裝程式的限制,從原始碼構建擴充套件時,您需要手動定義 Py_GIL_DISABLED=1

另請參閱

移植擴充套件模組以支援自由執行緒:一個由社群維護的擴充套件作者移植指南。