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_ITEM
和PyList_SET_ITEM
之類的訪問宏不執行任何錯誤檢查或鎖定。如果容器物件可能會被併發修改,則這些宏不是執行緒安全的。借用引用:如果包含物件被併發修改,則返回借用引用的 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 |
---|---|
無(參見 PyDict_Next) |
|
並非所有返回借用引用的 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
字尾表示。
pypa/manylinux 支援自由執行緒構建,帶有
t
字尾,例如python3.13t
。如果您設定了 CIBW_FREE_THREADED_SUPPORT,則 pypa/cibuildwheel 支援自由執行緒構建。
有限 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
。
另請參閱
移植擴充套件模組以支援自由執行緒:一個由社群維護的擴充套件作者移植指南。