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

Python 增強提案 (Python Enhancement Proposals)

PEP 684 – 每個直譯器一個 GIL

作者:
Eric Snow <ericsnowcurrently at gmail.com>
討論於:
Discourse 討論串
狀態:
最終 (Final)
類型:
標準軌跡 (Standards Track)
依賴項目:
683
建立日期:
2022年3月8日
Python 版本:
3.12
公告歷史:
2022年3月8日, 2022年9月29日, 2022年10月28日
決議:
Discourse 訊息

目錄

摘要

自 Python 1.5 (1997) 起,CPython 使用者即可在同一個行程中執行多個直譯器。然而,在同一個行程中的直譯器始終共享大量的全域狀態。這是錯誤的來源,且隨著越來越多人使用此功能,其影響日益顯著。此外,充分的隔離將有助於實現真正的多核心平行運算,屆時直譯器將不再共享 GIL。本提案中概述的變更將達成該層級的直譯器隔離。

高階摘要

總體而言,本提案以以下方式變更 CPython:

  • 在達成充分隔離的前提下,停止在直譯器之間共享 GIL
  • 新增數個用於隔離設定的直譯器組態選項
  • 防止不相容的擴充模組造成問題

GIL(全域直譯器鎖)

GIL 保護了對大部分 CPython 執行時期狀態的並發存取。因此,在 GIL 能夠分開之前,所有受 GIL 保護的全域狀態必須移至各個直譯器。

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

CPython 執行時期狀態

適當隔離直譯器需要將大部分 CPython 執行時期狀態儲存在 PyInterpreterState 結構中。目前僅有一部分儲存於此;其餘部分則存在於 C 全域變數或 _PyRuntimeState 中。其中大部分將必須進行遷移。

這與一項持續進行(多年)的工作不謀而合,即大幅減少全域變數的內部使用,並將執行時期狀態整合至 _PyRuntimeStatePyInterpreterState 中。(參見下方的「整合執行時期全域狀態」)。該專案本身具有顯著價值且幾乎沒有爭議。因此,儘管每個直譯器一個 GIL 的實現依賴該工作的完成,但該專案不應被視為本提案的一部分,而僅是一項依賴項。

其他隔離考量

CPython 的直譯器必須彼此嚴格隔離,除少數例外。在很大程度上,它們已經實現了隔離。每個直譯器都有自己的所有模組、類別、函式和變數副本。CPython C-API 文件有進一步說明

然而,除了上述已提及的部分(例如 GIL)之外,直譯器在某些方面仍會共享部分狀態。

首先,某些行程全域資源(例如記憶體、檔案描述符、環境變數)是共享的。目前沒有變更此項的計畫。

其次,由於錯誤或未考慮多直譯器的實作,某些隔離存在缺陷。這包括 CPython 的執行時期和標準函式庫,以及依賴全域變數的擴充模組。若遇到這些情況應回報錯誤,事實上已有部分錯誤被回報。

依賴「不朽物件」(Immortal Objects)

PEP 683 引入了作為 CPython 內部功能的「不朽物件」。有了不朽物件,我們可以在所有直譯器之間共享任何原本不可變的全域物件。因此,本 PEP 不需要解決如何處理 公開在公共 C-API 中的各類物件的問題。它也簡化了關於內建靜態型別處理方式的問題。(參見下方的「全域物件」)

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

動機

我們在此要解決的核心問題是 CPython 執行時期缺乏真正的多核心平行運算(針對 Python 程式碼)。GIL 是其原因所在。雖然在實務上通常不是問題,但至少它讓 Python 的多核心發展變得模糊不清,使得 GIL 成為一個持續存在的干擾因素。

隔離的直譯器也是支援特定並發模型的有效機制。PEP 554 對此有更詳細的討論。

間接效益

實現每個直譯器一個 GIL 所需的大部分工作,其本身就具備值得執行的價值:

  • 使多直譯器行為更可靠
  • 促成了長期存在的執行時期錯誤的修復,這些錯誤原本未被優先處理
  • 揭露(並激勵修復)了先前未知的執行時期錯誤
  • 推動了更乾淨的執行時期初始化(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
  • multiprocessing
    • 使其有效的代價過大;在某些情況下(大規模、Windows)效能損失嚴重
  • 其他平行運算工具(例如 dask, ray, MPI)
    • 不適合執行時期/標準函式庫
  • 放棄多核心(例如 async,什麼都不做)
    • 這註定會以失敗收場

即使在 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 (struct)
  • PyInterpreterConfig_INIT (macro)
  • PyInterpreterConfig_LEGACY_INIT (macro)
  • PyThreadState * Py_NewInterpreterFromConfig(PyInterpreterConfig *)

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

  • int own_gil
  • int strict_extensions_compat

未來我們可能會視需要新增其他欄位(例如 “own_initial_thread”)。

關於初始化巨集,PyInterpreterConfig_INIT 將用於取得一個隔離的直譯器,該直譯器同時避開不友善於子直譯器的功能。這將是透過 PEP 554 建立的直譯器的預設值。不受限制的(現狀)將繼續透過 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 時,此類模組必須用鎖來封裝該狀態的任何使用(例如透過呼叫)。

目前尚不清楚「支援多直譯器」是否足以等同於「支援每個直譯器一個 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 上。該物件不僅必須是有效不可變的(例如單例、字串),甚至其引用計數 (refcount) 也不能發生變更才算安全。不朽性(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