Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案

PEP 684 – 針對每個直譯器的 GIL

作者:
Eric Snow <ericsnowcurrently at gmail.com>
討論至:
Discourse 帖子
狀態:
最終版
型別:
標準跟蹤
要求:
683
建立日期:
2022年3月8日
Python 版本:
3.12
釋出歷史:
2022年3月8日, 2022年9月29日, 2022年10月28日
決議:
Discourse 訊息

目錄

摘要

自 Python 1.5 (1997) 以來,CPython 使用者可以在同一程序中執行多個直譯器。然而,同一程序中的直譯器始終共享大量全域性狀態。這是 bug 的來源,並且隨著越來越多的人使用該功能,其影響也越來越大。此外,充分的隔離將促進真正的多核並行,屆時直譯器不再共享 GIL。本提案中概述的更改將實現這種程度的直譯器隔離。

高層總結

從高層次來看,本提案以以下方式更改 CPython:

  • 在充分隔離的情況下,停止在直譯器之間共享 GIL
  • 為隔離設定新增多個新的直譯器配置選項
  • 防止不相容的擴充套件造成問題

GIL

GIL 保護對 CPython 大多數執行時狀態的併發訪問。因此,所有這些受 GIL 保護的全域性狀態都必須在 GIL 之前移到每個直譯器中。

(在少數情況下,可以使用其他機制來確保執行緒安全共享,例如鎖或“不朽”物件。)

CPython 執行時狀態

正確隔離直譯器需要將 CPython 的大部分執行時狀態儲存在 PyInterpreterState 結構中。目前,只有一部分儲存在那裡;其餘的要麼在 C 全域性變數中,要麼在 _PyRuntimeState 中。大部分都必須移動。

這與正在進行的(多年來的)努力直接相關,旨在大幅減少全域性變數的內部使用,並將執行時狀態整合到 _PyRuntimeStatePyInterpreterState 中。(參見下文的整合執行時全域性狀態。)該專案本身就具有重要的價值,並且幾乎沒有爭議。因此,雖然每個直譯器的 GIL 依賴於該工作的完成,但該專案不應被視為本提案的一部分——而僅僅是一個依賴。

其他隔離注意事項

CPython 的直譯器必須嚴格相互隔離,只有少數例外。在很大程度上,它們已經如此。每個直譯器都有自己的所有模組、類、函式和變數的副本。CPython C-API 文件進一步解釋

然而,除了已經提到的(例如 GIL),直譯器仍然以兩種方式共享一些狀態。

首先,一些程序全域性資源(例如記憶體、檔案描述符、環境變數)是共享的。目前沒有計劃改變這一點。

其次,由於錯誤或未考慮多直譯器實現的缺陷,某些隔離存在問題。這包括 CPython 的執行時和標準庫,以及依賴全域性變數的擴充套件模組。在這種情況下應提交 bug 報告,有些已經提交。

依賴於不朽物件

PEP 683 引入了不朽物件作為 CPython 的內部特性。透過不朽物件,我們可以共享所有直譯器之間所有其他不可變的全域性物件。因此,本 PEP 無需解決如何處理 公共 C-API 中公開的各種物件的問題。它還簡化了處理內建靜態型別的問題。(參見下面的全域性物件。)

這兩個問題都有替代解決方案,但使用不朽物件會使一切變得更簡單。如果 PEP 683 未被接受,那麼本提案將更新為替代方案。這使我們能夠減少本提案中的冗餘資訊。

動機

我們在此解決的根本問題是 CPython 執行時中缺乏真正的多核並行性(對於 Python 程式碼)。GIL 是其原因。雖然在實踐中通常不是問題,但至少它使 Python 的多核故事變得模糊不清,這使得 GIL 始終令人分心。

隔離的直譯器也是支援某些併發模型的有效機制。PEP 554 更詳細地討論了這一點。

間接好處

實現每個直譯器 GIL 所需的大部分工作都具有使其無論如何都值得做的好處:

  • 使多直譯器行為更可靠
  • 導致解決了長期存在的執行時 bug,這些 bug 以前未被優先處理
  • 暴露(並啟發修復)了以前未知的執行時 bug
  • 推動了更清晰的執行時初始化(PEP 432, PEP 587
  • 推動了更清晰、更完整的執行時終結
  • 導致了 C-API 的結構分層(例如 Include/internal
  • 另請參見下面的整合的好處

此外,其中大部分工作都惠及其他 CPython 相關專案:

多直譯器的現有用法

多年來,多直譯器的 C-API 一直在使用。然而,直到相對較近,該功能才廣為人知,也未被廣泛使用(mod_wsgi 除外)。

在過去幾年中,多直譯器的使用量有所增加。以下是一些目前使用該功能的公共專案:

請注意,有了 PEP 554,多直譯器的使用量可能會顯著增長(透過 Python 程式碼而不是 C-API)。

PEP 554(標準庫中的多直譯器)

PEP 554 嚴格來說是提供一個最小化的標準庫模組,讓使用者可以從 Python 程式碼訪問多個直譯器。事實上,它特意避免提出任何與 GIL 相關的更改。但是,請考慮該模組的使用者將受益於每個直譯器的 GIL,這使得 PEP 554 更具吸引力。

基本原理

在 2014 年的初步調查中,探索了各種可能的 Python 多核解決方案,但每個方案都有其缺點,沒有簡單的解決方案:

  • 在擴充套件模組中釋放 GIL 的現有做法
    • 對 Python 程式碼沒有幫助
  • 其他 Python 實現(例如 Jython、IronPython)
    • CPython 主導社群
  • 移除 GIL(例如 gilectomy,“no-gil”)
    • 技術風險太大(當時)
  • Trent Nelson 的 “PyParallel” 專案
    • 不完整;當時僅限 Windows
  • 多程序
    • 使其足夠有效的工作量太大;在某些情況下(大規模、Windows)懲罰很高
  • 其他並行工具(例如 dask、ray、MPI)
    • 不適合執行時/標準庫
  • 放棄多核(例如非同步、無所作為)
    • 這隻會以悲劇收場

即使在 2014 年,使用隔離直譯器的解決方案技術風險不高,而且大部分工作無論如何都值得做,這一點已經相當清楚。(缺點是要完成的工作量很大。)

規範

正如上面總結的,本提案涉及以下更改,按其必須發生的順序排列:

  1. 將全域性執行時狀態(包括物件)整合到 _PyRuntimeState
  2. 將幾乎所有狀態下移到 PyInterpreterState
  3. 最後,將 GIL 下移到 PyInterpreterState
  4. 其他一切
    • 更新 C-API
    • 實施擴充套件模組限制
    • 與流行的擴充套件維護者合作,幫助支援多直譯器

每個直譯器的狀態

以下執行時狀態將移至 PyInterpreterState

  • 所有不安全共享(完全不可變)的全域性物件
  • GIL
  • 當前受 GIL 保護的大多數可變資料
  • 當前受其他每個直譯器鎖保護的可變資料
  • 可在不同直譯器中獨立使用的可變資料(也適用於擴充套件模組,包括多階段初始化模組)
  • 所有其他未在下面排除的可變資料

此外,部分完整的全域性狀態已移至直譯器,包括 GC、警告和 atexit 鉤子。

以下執行時狀態不會被移動

  • 安全共享的全域性物件(如果有)
  • 不可變資料,通常是 const
  • 實際不可變資料(被視為不可變),例如
    • 某些狀態在早期初始化後不再修改
    • 字串的雜湊值 (PyUnicodeObject) 在首次需要時冪等計算,然後快取
  • 所有保證僅在主執行緒中修改的資料,包括
    • 僅在 CPython 的 main() 中使用的狀態
    • REPL 的狀態
    • 僅在執行時初始化期間修改的資料(之後實際不可變)
  • 受某些全域性鎖(GIL 除外)保護的可變資料
  • 原子變數中的全域性狀態
  • 可合理地更改為原子變數的可變全域性狀態

記憶體分配器

這是隔離直譯器工作中最敏感的部分之一。最簡單的解決方案是將內部“小塊”分配器的全域性狀態移至 PyInterpreterState,就像我們處理幾乎所有其他執行時狀態一樣。以下將詳細闡述其細節和原理。

CPython 提供了一個記憶體管理 C-API,具有三個分配器域:“raw”、“mem”和“object”。每個域都提供等效於 malloc()calloc()realloc()free() 的功能。可以在執行時初始化期間設定每個域的自定義分配器,並且可以使用相同的 API 包裝當前分配器以新增鉤子(例如,標準庫的 tracemalloc 模組)。分配器目前是執行時全域性的,由所有直譯器共享。

“raw”分配器預期是執行緒安全的,預設使用 glibc 的分配器(malloc() 等)。然而,“mem”和“object”分配器預期不是執行緒安全的,目前可能依賴 GIL 來保證執行緒安全。這部分是因為兩者的預設分配器,即“pyobject”,不是執行緒安全的。這是因為該分配器的所有狀態都儲存在 C 全域性變數中。(參見 Objects/obmalloc.c。)

因此,我們又回到了隔離執行時狀態的問題。為了讓直譯器停止共享 GIL,必須解決分配器的執行緒安全問題。如果直譯器繼續共享分配器,那麼我們需要其他方法來獲得執行緒安全。否則,直譯器必須停止共享分配器。在這兩種情況下,都有許多可能的解決方案,每種方案都有潛在的缺點。

為了保持分配器共享,最簡單的解決方案是在 PyMem_Malloc()PyObject_Malloc() 等函式中對“mem”和“object”分配器呼叫使用細粒度的執行時全域性鎖。這將影響效能,但有一些方法可以緩解(例如,只有在建立第一個子直譯器後才開始加鎖)。

另一種保持分配器共享的方法是要求“mem”和“object”分配器是執行緒安全的。這意味著我們必須使 pyobject 分配器的實現執行緒安全。這甚至可能涉及使用可擴充套件分配器(如 mimalloc)重新實現它。潛在的缺點是重新實現分配器的成本以及此類工作固有的缺陷風險。

無論如何,轉向要求執行緒安全的分配器將影響所有嵌入 CPython 且當前設定了非執行緒安全分配器的人。我們需要考慮誰可能受到影響以及如何減少任何負面影響(例如,新增一個基本的 C-API 來幫助使分配器執行緒安全)。

如果我們確實停止了在直譯器之間共享分配器,那麼我們必須只對“mem”和“object”分配器這樣做。我們可能還需要保留一組完整的全域性分配器用於某些執行時級別的使用。由於查詢當前直譯器然後透過指標間接獲取分配器,這將帶來一些效能損失。嵌入者也可能需要為每個直譯器提供一個新的分配器上下文。從好的方面看,分配器鉤子(例如 tracemalloc)不會受到影響。

最終,我們將選擇最簡單的方案:

  • 將分配器保留在全域性執行時狀態中
  • 要求它們是執行緒安全的
  • 將預設物件分配器(又稱“小塊”分配器)的狀態移至 PyInterpreterState

我們嘗試了一個粗略的實現,發現它相當簡單,而且效能損失基本為零。

C-API

在內部,直譯器狀態現在將跟蹤匯入系統應如何處理不支援與多個直譯器一起使用的擴充套件模組。參見下面的限制擴充套件模組。我們在此將其稱為“PyInterpreterState.strict_extension_compat”。

以下 API 將公開,如果尚未公開的話:

  • PyInterpreterConfig(結構體)
  • PyInterpreterConfig_INIT(宏)
  • PyInterpreterConfig_LEGACY_INIT(宏)
  • PyThreadState * Py_NewInterpreterFromConfig(PyInterpreterConfig *)

我們將在 PyInterpreterConfig 中新增兩個新欄位

  • int own_gil
  • int strict_extensions_compat

隨著時間的推移,我們可能會根據需要新增其他欄位(例如“own_initial_thread”)。

關於初始化宏,PyInterpreterConfig_INIT 將用於獲取一個隔離的直譯器,該直譯器同時避免了對子直譯器不友好的功能。它將是PEP 554 中透過 Python 程式碼建立直譯器的預設設定。不受限制的(現狀)將繼續透過 PyInterpreterConfig_LEGACY_INIT 提供,該宏已用於主直譯器和 Py_NewInterpreter()。這不會改變。

關於“主”直譯器的一點說明

下文我們多次提到“主”直譯器。這指的是在執行時初始化期間建立的直譯器,其初始 PyThreadState 對應於程序的主執行緒。它具有許多獨特的職責(例如處理訊號),並在執行時初始化/終結期間扮演著特殊角色。它通常(目前)也是唯一的直譯器。(另請參見 https://docs.python.club.tw/3/c-api/init.html#sub-interpreter-support。)

PyInterpreterConfig.own_gil

如果為 true (1),則新直譯器將擁有自己的“全域性”直譯器鎖。這意味著新直譯器可以在不被其他直譯器中斷的情況下執行。這有效地解除了多核的全部使用障礙。這是本 PEP 的根本目標。

如果為 false (0),則新直譯器將使用主直譯器的鎖。這是 CPython 中遺留的(3.12 之前的)行為,所有直譯器共享一個 GIL。當使用仍然依賴 GIL 進行執行緒安全的擴充套件模組時,這樣共享 GIL 可能是可取的。

PyInterpreterConfig_INIT 中,這會是 true。在 PyInterpreterConfig_LEGACY_INIT 中,這會是 false

另外,為了安全起見,目前我們不允許在執行時初始化期間設定了自定義分配器的情況下將 own_gil 設定為 true。包裝分配器(例如 tracemalloc)仍然可以正常工作。

PyInterpreterConfig.strict_extensions_compat

PyInterpreterConfig.strict_extension_compat 基本上是用於“PyInterpreterState.strict_extension_compat”的初始值。

限制擴充套件模組

當狀態儲存在全域性變數中時,擴充套件模組與執行時存在許多相同的問題。PEP 630 涵蓋了擴充套件必須做些什麼才能支援隔離,從而安全地在多個直譯器中同時執行的所有細節。這包括處理它們的全域性變數。

如果一個擴充套件實現了多階段初始化(參見 PEP 489),則它被認為是與多個直譯器相容的。所有其他擴充套件都被認為是不相容的。(有關每個直譯器 GIL 如何影響該分類的更多詳細資訊,請參見擴充套件模組執行緒安全。)

如果匯入了不相容的擴充套件,並且當前“PyInterpreterState.strict_extension_compat”值為 true,則匯入系統將引發 ImportError。(對於 false,它只是不檢查。)這將透過 importlib._bootstrap_external.ExtensionFileLoader 完成(實際上,透過 _imp.create_dynamic()_PyImport_LoadDynamicModuleWithSpec()PyModule_FromDefAndSpec2())。

在主直譯器中(或透過 Py_NewInterpreter() 建立的直譯器中),此類匯入永遠不會失敗,因為在這兩種情況下,“PyInterpreterState.strict_extension_compat”都初始化為 false。因此,保留了遺留的(3.12 之前的)行為。

我們將與流行的擴充套件合作,幫助它們支援在多個直譯器中使用。這可能涉及新增到 CPython 的公共 C-API,我們將根據具體情況進行處理。

擴充套件模組相容性

正如擴充套件模組中指出的,許多擴充套件在多個直譯器中(以及在每個直譯器 GIL 下)都能正常工作,而無需進行任何更改。如果此類模組未明確指示支援,匯入系統仍將失敗。最初,許多擴充套件模組都不會這樣做,因此這可能是令人沮喪的來源。

我們將透過新增一個上下文管理器來解決這個問題,以暫時停用對多直譯器支援的檢查:importlib.util.allow_all_extensions()。或多或少,它將修改當前“PyInterpreterState.strict_extension_compat”的值(例如,透過一個私有的 sys 函式)。

擴充套件模組執行緒安全

如果一個模組支援與多個直譯器一起使用,那主要意味著即使這些直譯器不共享 GIL,它也能正常工作。唯一的例外是當模組連結到一個內部全域性狀態非執行緒安全的庫時。(即使是像靜態區域性變數作為臨時緩衝區這樣無害的東西也可能是一個問題。)有了共享的 GIL,該狀態受到保護。如果沒有,此類模組必須使用鎖包裝對該狀態的任何使用(例如透過呼叫)。

目前尚不清楚支援多直譯器是否與支援每個直譯器的 GIL 充分等效,以至於我們可以避免任何特殊安排。這仍然是一個有意義的討論和調查點。兩者之間的實際區別(在 Python 社群中,例如 PyPI)尚未完全理解,無法解決此問題。同樣,尚不清楚我們能做些什麼來幫助擴充套件維護者緩解問題(假設這是一個問題)。

與此同時,我們必須假設這種差異足以給現有足夠多的擴充套件模組帶來問題。我們將採用的解決方案是:

  • 新增一個 PyModuleDef 槽位,指示擴充套件可以在每個直譯器 GIL 下匯入(即選擇加入)
  • 將該槽位作為“相容”擴充套件定義的一部分,如前所述

缺點是,如果沒有模組維護者額外付出努力,即使是微小的努力,任何擴充套件模組都無法利用每個直譯器 GIL。這使得擴充套件模組相容性中描述的問題更加複雜,並且適用相同的解決方法。理想情況下,我們會確定差異不足以構成問題。

如果最終我們確實要求在每個直譯器 GIL 下匯入時選擇加入,並且稍後確定沒有必要,那麼我們可以屆時切換預設設定,使舊的選擇加入槽位成為空操作,並新增一個新的 PyModuleDef 槽位用於明確選擇 *退出*。事實上,從一開始就新增該選擇退出槽位是很有意義的。

文件

  • C-API:Doc/c-api/init.rst 的“子直譯器支援”部分將詳細說明更新後的 API
  • C-API:該部分將解釋每個直譯器 GIL 的後果
  • importlib:ExtensionFileLoader 條目將註明匯入可能在子直譯器中失敗
  • importlib:將有一個關於 importlib.util.allow_all_extensions() 的新條目

影響

向後相容性

本提案無意改變任何行為或 API,但有兩個例外:

  • 某些擴充套件將在某些子直譯器中匯入失敗(參見下一節
  • 當前非執行緒安全的“mem”和“object”分配器現在在與多個直譯器結合使用時可能容易受到資料競爭的影響

現有的用於管理直譯器的 C-API 將保留其當前行為,新行為將透過新的 API 公開。不打算更改任何其他 API 或執行時行為,包括與穩定 ABI 的相容性。

有關相關討論,請參閱下面的C-API 中公開的物件

擴充套件模組

目前,Python 最常見的用法是主直譯器單獨執行。本提案對這種場景下的擴充套件模組沒有任何影響。同樣,無論好壞,使用現有 Py_NewInterpreter() 建立的多個直譯器下的行為沒有改變。

請記住,一些擴充套件在多個直譯器中使用時已經出現問題,因為它們將模組狀態儲存在全域性變數中(或由於連結庫的內部狀態)。它們可能會崩潰,甚至更糟,出現不一致的行為。這正是PEP 630 等提案的動機之一,因此這不是一個新的情況,也不是本提案的後果。

相反,當使用提議的 API 並使用適當設定建立多個直譯器時,不相容擴充套件的行為將發生變化。在這種情況下,匯入此類擴充套件將失敗(在主直譯器之外),如限制擴充套件模組中所述。對於已經在多個直譯器中崩潰的擴充套件,這將是一個改進。

此外,一些擴充套件模組連結到具有非執行緒安全內部全域性狀態的庫。(參見擴充套件模組執行緒安全。)此類模組將不得不開始用鎖包裝對該狀態的任何直接或間接使用。這是與也實現多階段初始化並因此指示支援多直譯器(即隔離)的其他模組的關鍵區別。

現在我們來談談上面提到的相容性問題。有些擴充套件在多個直譯器下(以及在每個直譯器的 GIL 下)是安全的,儘管它們沒有表明這一點。不幸的是,匯入系統無法可靠地推斷出此類擴充套件是安全的,因此匯入它們仍將失敗。這種情況在上面的擴充套件模組相容性中有所解決。

擴充套件模組維護者

一個相關的考慮是,每個直譯器的 GIL 可能會推動多直譯器使用量的增加,特別是如果 PEP 554 被接受。一些大型擴充套件模組的維護者對因多直譯器使用量增加而帶來的負擔表示擔憂。

具體來說,為某些擴充套件模組(儘管可能不多)啟用多直譯器支援將需要大量工作。為了新增該支援,此類模組的維護者(通常是志願者)將不得不擱置他們的正常優先順序和興趣,專注於相容性(參見 PEP 630)。

當然,擴充套件維護者可以自由選擇不新增對多直譯器使用的支援。但是,使用者將越來越多地要求這種支援,特別是如果該功能越來越受歡迎。

無論如何,這種情況對於此類擴充套件的維護者來說可能會感到壓力,特別是當他們在業餘時間完成工作時。他們表達的擔憂是可以理解的,我們將在限制擴充套件模組擴充套件模組相容性部分討論部分解決方案。

備選 Python 實現

其他 Python 實現不要求在同一程序中提供多直譯器支援(儘管有些已經提供)。

安全隱患

本提案對安全性沒有已知影響。

可維護性

一方面,本提案已經推動了多項改進,使 CPython *更*易於維護。預計這種情況將持續下去。另一方面,基礎工作已經暴露了執行時中各種預先存在的缺陷,這些缺陷必須得到修復。隨著多直譯器使用量的增加,預計這種情況也將持續下去。否則,對可維護性不應有顯著影響,因此淨效應應是積極的。

效能

整合全域性變數的工作已經為 CPython 的效能帶來了多項改進,既提高了速度又減少了記憶體使用,預計這將繼續下去。具體而言,每個直譯器 GIL 的效能優勢尚未探索。至少,預計它不會使 CPython 變慢(只要直譯器充分隔離)。而且,顯然,它將使 Python 程式碼中的各種多核並行成為可能。

如何教授此內容

PEP 554 不同,這是一項針對少數 C-API 使用者的高階功能。不期望教授 API 的具體細節或其直接應用。

也就是說,如果確實要教授,則歸結為以下幾點:

除了 Py_NewInterpreter(),您還可以使用 Py_NewInterpreterFromConfig() 來建立直譯器。您傳遞的配置指示了您希望該直譯器如何行為。

此外,建立隔離直譯器的任何擴充套件模組的維護者可能需要向其使用者解釋每個直譯器 GIL 的後果。首先要解釋的是 PEP 554 所教授的隔離直譯器所支援的併發模型。這引出了一個觀點:使用該併發模型編寫的 Python 軟體可以利用多核並行,而這目前被 GIL 阻止。

參考實現

<待定>

未解決的問題

  • 我們是否可以要求“mem”和“object”分配器是執行緒安全的?
  • 每個直譯器的 tracemalloc 模組將如何與全域性分配器相關聯?
  • faulthandler 模組會侷限於主直譯器(像訊號模組一樣)嗎?還是我們會讓全域性狀態在直譯器之間洩漏(受細粒度鎖保護)?
  • 根據“整合執行時全域性狀態”部分拆分出一個包含所有相關資訊的說明性 PEP 嗎?
  • 一個模組在多個直譯器下(隔離)可以工作,但在每個直譯器 GIL 下卻不能工作的可能性有多大?(參見擴充套件模組執行緒安全。)
  • 如果可能性足夠大,我們能做些什麼來幫助擴充套件維護者緩解問題並享受在每個直譯器 GIL 下使用的樂趣?
  • allow_all_extensions 有沒有更好的(聽起來更可怕的)名稱?

延遲的功能

  • PyInterpreterConfig 選項,用於始終在新執行緒中執行直譯器
  • PyInterpreterConfig 選項,用於將“主”執行緒分配給直譯器,並且僅在該執行緒中執行

被拒絕的想法

<待定>

額外上下文

共享全域性物件

我們正在直譯器之間共享一些全域性物件。這是一個實現細節,與全域性變數整合而非本提案更相關,但它是一個足夠重要的細節,需要在此解釋。

另一種選擇是永遠不在直譯器之間共享任何物件。為了實現這一點,我們必須解決所有靜態型別的命運,並處理許多在公共 C-API 中公開的物件的相容性問題。

這種方法引入了大量的額外複雜性和更高的風險,儘管原型設計已經證明了有效的解決方案。此外,它可能會導致效能損失。

不朽物件允許我們共享其他不可變的全域性物件。這樣我們就可以避免額外的開銷。

C-API 中公開的物件

C-API(包括受限 API)公開了所有內建型別,包括內建異常,以及內建單例。異常以 PyObject * 的形式公開,但其餘以靜態值而非指標的形式公開。這是我們必須為每個直譯器 GIL 解決的少數非平凡問題之一。

有了不朽物件,這就不再是個問題了。

整合執行時全域性狀態

正如上面CPython 執行時狀態中提到的,正在進行一項積極的工作(與本 PEP 無關),旨在將 CPython 的全域性狀態整合到 _PyRuntimeState 結構中。幾乎所有工作都涉及將該狀態從全域性變數中移出。該專案與本提案特別相關,因此下面將提供一些額外細節。

整合的好處

整合全域性變數有多種好處:

  • 大大減少了 C 全域性變數的數量(C 程式碼的最佳實踐)
  • 此舉將執行時狀態不穩定或損壞的情況提請注意
  • 鼓勵執行時狀態使用方式更加一致
  • 更容易發現/識別 CPython 的執行時狀態
  • 更容易以一致的方式靜態分配執行時狀態
  • 更好的執行時狀態記憶體區域性性

此外,上面間接好處中列出的所有好處也適用於此處,並且那裡列出的相同專案也會受益。

工作量

需要移動的全域性變數數量足夠大,但大多數是 Python 物件,可以大量處理(如 Py_IDENTIFIER)。在幾乎所有情況下,將這些全域性變數移至直譯器都是高度機械化的。這不需要巧妙,而是需要有人投入時間。

待移動的狀態

其餘全域性變數可分類如下:

  • 全域性物件
    • 靜態型別(包括異常型別)
    • 非靜態型別(包括堆型別、structseq 型別)
    • 單例(靜態)
    • 單例(一次初始化)
    • 快取物件
  • 非物件
    • 初始化後不會(或不太可能)更改
    • 僅在主執行緒中使用
    • 延遲初始化
    • 預分配緩衝區
    • 狀態

這些全域性變數分散在核心執行時、內建模組和標準庫擴充套件模組之間。

要了解剩餘全域性變數的詳細分類,請執行:

./python Tools/c-analyzer/table-file.py Tools/c-analyzer/cpython/globals-to-fix.tsv

已完成的工作

如前所述,這項工作已經進行了多年。以下是一些已經完成的事情:

  • 執行時初始化清理(參見 PEP 432 / PEP 587
  • 擴充套件模組隔離機制(參見 PEP 384 / PEP 3121 / PEP 489
  • 許多內建模組的隔離
  • 許多標準庫擴充套件模組的隔離
  • 添加了 _PyRuntimeState
  • 不再有 _Py_IDENTIFIER()
  • 靜態分配
    • 空字串
    • 字串字面量
    • 識別符號
    • latin-1 字串
    • 長度為 1 的位元組
    • 空元組

工具

如前所述,有幾個工具可以幫助識別全域性變數並對其進行推理。

  • Tools/c-analyzer/cpython/globals-to-fix.tsv - 剩餘全域性變數列表
  • Tools/c-analyzer/c-analyzer.py
    • analyze - 識別所有全域性變數
    • check - 如果存在任何未被忽略的不支援的全域性變數,則失敗
  • Tools/c-analyzer/table-file.py - 彙總已知全域性變數

此外,對不支援的全域性變數的檢查已整合到 CI 中,以防止意外新增新的全域性變數。

全域性物件

可以安全地在直譯器之間共享(無需 GIL)的全域性物件可以保留在 _PyRuntimeState 上。物件不僅必須是實際不可變的(例如單例、字串),而且即使引用計數也不能更改,才能保證安全。不朽物件 (PEP 683) 提供了這一點。(另一種選擇是根本不共享物件,這會大大增加解決方案的複雜性,特別是對於在公共 C-API 中公開的物件。)

內建靜態型別是全域性物件的一種特殊情況,它們將被共享。除了一個部分外,它們實際上是不可變的:__subclasses__(又稱 tp_subclasses)。我們預期內建型別的其他任何部分都不會改變,即使是 __dict__(又稱 tp_dict)的內容也不會改變。

內建型別的 __subclasses__ 將透過將其設定為一個 getter 來處理,該 getter 在當前 PyInterpreterState 中查詢該型別。

參考資料

相關內容

  • PEP 384 “定義穩定 ABI”
  • PEP 432 “重構 CPython 啟動序列”
  • PEP 489 “多階段擴充套件模組初始化”
  • PEP 554 “標準庫中的多直譯器”
  • PEP 573 “從 C 擴充套件方法訪問模組狀態”
  • PEP 587 “Python 初始化配置”
  • PEP 630 “隔離擴充套件模組”
  • PEP 683 “不朽物件,使用固定引用計數”
  • PEP 3121 “擴充套件模組初始化和終結”

來源: https://github.com/python/peps/blob/main/peps/pep-0684.rst

最後修改時間: 2024-06-04 17:05:36 GMT