PEP 684 – 인터프리터별 GIL
- Author:
- Eric Snow <ericsnowcurrently at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Type:
- Standards Track
- Requires:
- 683
- Created:
- 08-Mar-2022
- Python-Version:
- 3.12
- Post-History:
- 08-Mar-2022, 29-Sep-2022, 28-Oct-2022
- Resolution:
- Discourse message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
Python 1.5(1997)부터 CPython 사용자는 동일한 프로세스에서 여러 인터프리터를 실행할 수 있습니다. 그러나 동일한 프로세스의 인터프리터들은 상당한 양의 전역 상태를 항상 공유해 왔습니다. 이는 버그의 원인이 되며, 이 기능을 사용하는 사람이 점점 늘어남에 따라 그 영향도 커지고 있습니다. 또한 충분한 격리를 통해 인터프리터가 더 이상 GIL을 공유하지 않는 진정한 멀티코어 병렬 처리를 구현할 수 있습니다. 이 제안에서 설명하는 변경 사항을 통해 이러한 수준의 인터프리터 격리가 이루어집니다.
고수준 요약
고수준에서 이 제안은 다음과 같은 방식으로 CPython을 변경합니다.
- 충분한 격리가 이루어지면 인터프리터 간 GIL 공유를 중단합니다.
- 격리 설정을 위한 몇 가지 새로운 인터프리터 구성 옵션을 추가합니다.
- 호환되지 않는 확장 기능으로 인해 문제가 발생하지 않도록 합니다.
GIL
GIL은 CPython 런타임 상태 대부분에 대한 동시 접근을 보호합니다. 따라서 GIL로 보호되는 모든 전역 상태를 GIL을 각 인터프리터로 옮길 수 있기 전에 각 인터프리터로 이동해야 합니다.
(몇몇 경우에는 잠금이나 “불멸” 객체와 같은 다른 메커니즘을 대신 사용하여 스레드 안전한 공유를 보장할 수 있습니다.)
CPython 런타임 상태
인터프리터를 적절히 격리하려면 CPython 런타임 상태 대부분을 PyInterpreterState 구조체에 저장해야 합니다. 현재는 그중 일부만 저장되어 있으며, 나머지는 C 전역 변수나 _PyRuntimeState에 있습니다. 이 중 대부분은 이동해야 합니다.
이는 전역 변수의 내부 사용을 크게 줄이고 런타임 상태를 _PyRuntimeState 및 PyInterpreterState로 통합하기 위해 수년간 진행 중인 작업과 직접 일치합니다. (자세한 내용은 아래의 Consolidating Runtime Global State를 참조하십시오.) 이 프로젝트는 significant merit on its own이 있으며 논란도 거의 없었습니다. 따라서 인터프리터별 GIL은 해당 작업의 완료에 의존하지만, 그 프로젝트를 이 제안의 일부가 아니라 종속 항목으로만 간주해야 합니다.
기타 격리 고려 사항
CPython의 인터프리터는 몇 가지 예외를 제외하고 서로 엄격하게 격리되어야 합니다. 대체로 이미 그렇게 격리되어 있습니다. 각 인터프리터는 모든 모듈, 클래스, 함수 및 변수에 대한 자체 복사본을 가집니다. CPython C-API 문서는 explain further에서 더 자세히 설명합니다.
그러나 앞서 언급한 사항(예: GIL)을 제외하면 인터프리터가 여전히 일부 상태를 공유하는 방식이 몇 가지 있습니다.
무엇보다도 일부 프로세스 전역 리소스(예: 메모리, 파일 디스크립터, 환경 변수)가 공유됩니다. 이를 변경할 계획은 없습니다.
둘째, 다중 인터프리터를 고려하지 않은 버그나 구현으로 인해 일부 격리가 제대로 작동하지 않습니다. 여기에는 CPython의 런타임과 표준 라이브러리뿐만 아니라 전역 변수에 의존하는 확장 모듈도 포함됩니다. 이러한 경우에는 버그를 등록해야 하며, 이미 일부는 등록되어 있습니다.
불멸 객체에 대한 의존
PEP 683은 CPython 내부 기능으로 불멸 객체를 도입합니다. 불멸 객체를 사용하면, 그 외에는 변경할 수 없는 전역 객체를 모든 인터프리터 간에 공유할 수 있습니다. 따라서 이 PEP에서는 공개 C-API에 exposed in the public C-API 노출된 다양한 객체를 처리하는 방법을 다룰 필요가 없습니다. 또한 내장 정적 타입에 대해 어떻게 처리할지라는 문제도 단순해집니다. (아래의 Global Objects를 참조하십시오.)
두 문제 모두 다른 해결책이 있지만, 불멸 객체를 사용하면 모든 것이 더 단순해집니다. PEP 683이 수용되지 않으면 이 PEP를 대안과 함께 갱신합니다. 이를 통해 이 제안의 불필요한 잡음을 줄일 수 있습니다.
동기
여기에서 해결하려는 근본적인 문제는 CPython 런타임에서 진정한 멀티코어 병렬 처리(파이썬 코드의 경우)가 부족하다는 점입니다. 원인은 GIL입니다. 일반적으로 실제 사용에서는 문제가 되지 않지만, 적어도 파이썬의 멀티코어 관련 상황을 불분명하게 만들며, 이로 인해 GIL은 지속적인 방해 요소가 됩니다.
격리된 인터프리터는 특정 동시성 모델을 지원하는 효과적인 메커니즘이기도 합니다. PEP 554에서 이 내용을 더 자세히 설명합니다.
간접적인 이점
인터프리터별 GIL에 필요한 대부분의 작업에는 어차피 수행할 가치가 있게 만드는 이점이 있습니다.
- 다중 인터프리터 동작의 신뢰성을 높입니다.
- 그렇지 않았다면 우선순위가 지정되지 않았을 장기간 지속된 런타임 버그의 수정으로 이어졌습니다.
- 이전에는 알려지지 않았던 런타임 버그를 드러내고 이를 수정하도록 이끌었습니다.
- 더 깔끔한 런타임 초기화를 이끌었습니다(PEP 432, PEP 587).
- 더 깔끔하고 완전한 런타임 종료를 이끌었습니다.
- C-API의 구조적 계층화를 이끌었습니다(예:
Include/internal). - 아래의 Benefits to Consolidation도 참조하십시오.
또한 그 작업의 상당 부분은 다른 CPython 관련 프로젝트에도 도움이 됩니다.
- 성능 개선(”faster-cpython”)
- 포크 전 애플리케이션 배포(예: Instagram server)
- 확장 모듈 격리(PEP 630 등 참조)
- CPython 임베딩
다중 인터프리터의 기존 사용 방식
다중 인터프리터를 위한 C-API는 수년간 사용되어 왔습니다. 그러나 비교적 최근까지는 이 기능이 널리 알려지지 않았고, mod_wsgi를 제외하면 광범위하게 사용되지도 않았습니다.
지난 몇 년 동안 다중 인터프리터의 사용이 증가해 왔습니다. 현재 이 기능을 사용하는 공개 프로젝트는 다음과 같습니다.
참고로, PEP 554를 사용하면 C-API가 아닌 Python 코드를 통해 여러 인터프리터를 사용하는 일이 크게 증가할 가능성이 높습니다.
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년에도 격리된 인터프리터를 사용하는 해결책은 기술적 위험 수준이 높지 않았고, 대부분의 작업은 어차피 수행할 가치가 있다는 점이 상당히 분명했습니다. (단점은 수행해야 할 작업의 규모였습니다.)
명세
위에 요약한 내용에서, 이 제안에는 다음 변경 사항이 반드시 수행되어야 하는 순서대로 포함됩니다.
- 전역 런타임 상태 (객체 포함)를
_PyRuntimeState로 통합합니다. - 상태의 거의 전부를
PyInterpreterState로 옮깁니다. - 마지막으로 GIL을
PyInterpreterState로 옮깁니다. - 그 밖의 모든 항목
- C-API를 업데이트합니다.
- 확장 모듈 제한을 구현합니다.
- 다중 인터프리터 지원을 돕기 위해 주요 확장 모듈 유지 관리자들과 협력합니다.
인터프리터별 상태
다음 런타임 상태는 PyInterpreterState로 이동합니다.
- 안전하게 공유할 수 없는 모든 전역 객체(완전히 불변인 객체)
- GIL
- 현재 GIL로 보호되는 대부분의 변경 가능한 데이터
- 현재 다른 인터프리터별 잠금으로 보호되는 변경 가능한 데이터
- 서로 다른 인터프리터에서 독립적으로 사용할 수 있는 변경 가능한 데이터(다단계 초기화를 사용하는 확장 모듈을 포함한 확장 모듈에도 적용됩니다)
- 아래에서 달리 제외되지 않은 그 밖의 모든 변경 가능한 데이터
또한 전체 전역 상태의 일부는 이미 인터프리터로 이동되었으며, 여기에는 GC, 경고 및 atexit 후크가 포함됩니다.
다음 런타임 상태는 이동하지 않습니다.
- 안전하게 공유할 수 있는 전역 객체(있는 경우)
- 대개
const인 불변 데이터 - 사실상 불변인 데이터(불변으로 취급됨), 예를 들면 다음과 같습니다.
- 일부 상태는 초기에 초기화된 후 다시 수정되지 않습니다.
- 문자열(
PyUnicodeObject)의 해시는 처음 필요할 때 멱등적으로 계산된 후 캐시됩니다.
- 메인 스레드에서만 수정되는 것이 보장되는 모든 데이터(다음을 포함합니다):
- CPython의
main()에서만 사용되는 상태 - REPL의 상태
- 런타임 초기화 중에만 수정되는 데이터(그 이후에는 사실상 불변)
- CPython의
- GIL 이외의 일부 전역 잠금으로 보호되는 변경 가능한 데이터
- 원자 변수에 저장된 전역 상태
- 원자 변수로 합리적으로 변환할 수 있는 변경 가능한 전역 상태
메모리 할당자
인터프리터를 격리하는 작업에서 가장 민감한 부분 중 하나입니다. 가장 단순한 해결책은 다른 거의 모든 런타임 상태에 대해 수행하고 있는 것처럼, 내부 “small block” 할당자의 전역 상태를 PyInterpreterState로 옮기는 것입니다. 다음에서는 세부 사항과 그 근거를 자세히 설명합니다.
CPython은 three allocator domains를 사용하는 메모리 관리 C-API를 제공합니다: “raw”, “mem”, “object”입니다. 각각은 malloc(), calloc(), realloc(), free()에 해당하는 기능을 제공합니다. 런타임 초기화 중 각 도메인에 대한 사용자 정의 할당자를 설정할 수 있으며, 동일한 API를 사용하여 현재 할당자를 훅으로 래핑할 수 있습니다(예: 표준 라이브러리 tracemalloc 모듈). 현재 할당자는 런타임 전역이며 모든 인터프리터가 공유합니다.
“raw” 할당자는 스레드 안전할 것으로 예상되며 glibc의 할당자(malloc(), 등)를 기본값으로 사용합니다. 그러나 “mem” 및 “object” 할당자는 스레드 안전할 것으로 예상되지 않으며 현재 스레드 안전성을 위해 GIL에 의존할 수 있습니다. 이는 두 할당자의 기본 할당자, 즉 “pyobject”가 is not thread-safe 이기 때문이기도 합니다. 이는 해당 할당자의 모든 상태가 C 전역 변수에 저장되는 방식 때문입니다. (Objects/obmalloc.c를 참조하십시오.)
따라서 런타임 상태를 격리하는 문제로 다시 돌아옵니다. 인터프리터가 GIL 공유를 중단하려면 할당자의 스레드 안전성을 해결해야 합니다. 인터프리터가 할당자를 계속 공유한다면 스레드 안전성을 확보할 다른 방법이 필요합니다. 그렇지 않으면 인터프리터가 할당자 공유를 중단해야 합니다. 두 경우 모두 여러 가지 가능한 해결책이 있으며, 각각 잠재적인 단점이 있습니다.
할당자를 계속 공유하려면 가장 단순한 해결책은 PyMem_Malloc(), PyObject_Malloc(), 등의 “mem” 및 “object” 할당자 호출 주위에 세분화된 런타임 전역 잠금을 사용하는 것입니다. 이는 성능에 영향을 주지만, 이를 완화할 방법이 일부 있습니다(예: 첫 번째 서브인터프리터가 생성된 후에만 잠금을 시작하는 방법).
할당자를 계속 공유하는 또 다른 방법은 “mem” 및 “object” 할당자가 스레드 안전하도록 요구하는 것입니다. 이는 pyobject 할당자 구현을 스레드 안전하게 만들어야 한다는 의미입니다. 여기에는 mimalloc과 같은 확장 가능한 할당자를 사용하여 이를 다시 구현하는 작업도 포함될 수 있습니다. 잠재적인 단점은 할당자를 다시 구현하는 데 드는 비용과 이러한 작업에 내재된 결함 위험입니다.
어쨌든 스레드 안전한 할당자를 요구하도록 전환하면 CPython을 임베드하고 현재 스레드 안전하지 않은 할당자를 설정하는 모든 사용자에게 영향을 미칩니다. 누가 영향을 받을 수 있는지와 부정적인 영향을 줄일 방법을 고려해야 합니다(예: 할당자를 스레드 안전하게 만드는 데 도움이 되는 기본 C-API를 추가하는 방법).
인터프리터 간 할당자 공유를 중단한다면 “mem” 및 “object” 할당자에 대해서만 중단해야 합니다. 특정 런타임 수준의 용도를 위해 전역 할당자의 전체 집합을 유지해야 할 수도 있습니다. 현재 인터프리터를 조회한 다음 포인터 간접 참조를 통해 할당자를 가져와야 하므로 어느 정도 성능 저하가 발생합니다. 임베더는 각 인터프리터에 새로운 할당자 컨텍스트도 제공해야 할 가능성이 높습니다. 긍정적인 측면으로는 할당자 훅(예: tracemalloc)이 영향을 받지 않습니다.
궁극적으로 가장 단순한 옵션을 선택합니다:
- 할당자를 전역 런타임 상태에 유지합니다
- 할당자가 스레드 안전하도록 요구합니다
- 기본 객체 할당자(즉, “small block” 할당자)의 상태를
PyInterpreterState로 옮깁니다
a rough implementation을 실험해 보니 상당히 간단했으며 성능 저하는 사실상 0이었습니다.
C-API
이제 인터프리터 상태에서 여러 인터프리터와 함께 사용할 수 없는 확장 모듈을 임포트 시스템이 어떻게 처리해야 하는지 추적합니다. 아래의 Restricting Extension Modules 을 참조하십시오. 여기서는 해당 설정을 “PyInterpreterState.strict_extension_compat”라고 부르겠습니다.
다음 API는 아직 공개되지 않았다면 공개됩니다.
PyInterpreterConfig(구조체)PyInterpreterConfig_INIT(매크로)PyInterpreterConfig_LEGACY_INIT(매크로)PyThreadState * Py_NewInterpreterFromConfig(PyInterpreterConfig *)
PyInterpreterConfig에 두 개의 새 필드를 추가합니다.
int own_gilint strict_extensions_compat
필요에 따라 시간이 지나면서 다른 필드도 추가할 수 있습니다(예: “own_initial_thread”).
초기화 매크로와 관련하여 PyInterpreterConfig_INIT은 서브인터프리터에 적합하지 않은 기능도 피하는 격리된 인터프리터를 가져오는 데 사용됩니다. 이는 PEP 554를 통해 생성되는 인터프리터의 기본값이 됩니다. 제한이 없는 기존 동작은 이미 메인 인터프리터와 Py_NewInterpreter()에 사용되는 PyInterpreterConfig_LEGACY_INIT을 통해 계속 사용할 수 있습니다. 이는 변경되지 않습니다.
“main” 인터프리터에 대한 참고 사항:
아래에서는 “main” 인터프리터를 여러 차례 언급합니다. 이는 런타임 초기화 중 생성되는 인터프리터를 가리키며, 이 인터프리터의 초기 PyThreadState는 프로세스의 주 스레드에 해당합니다. 이 인터프리터에는 고유한 여러 책임(예: 신호 처리)이 있으며, 런타임 초기화 및 종료 중 특별한 역할도 수행합니다. 또한 일반적으로(현재로서는) 유일한 인터프리터입니다. (또한 https://docs.python.org/3/c-api/init.html#sub-interpreter-support 를 참조하십시오.)
PyInterpreterConfig.own_gil
true (1)이면 새 인터프리터는 자체 “전역” 인터프리터 잠금을 갖습니다. 이는 새 인터프리터가 다른 인터프리터에 의해 중단되지 않고 실행될 수 있음을 의미합니다. 이로써 여러 코어를 완전히 사용할 수 있게 됩니다. 이것이 이 PEP의 근본적인 목표입니다.
false (0)이면 새 인터프리터는 메인 인터프리터의 잠금을 사용합니다. 이는 CPython에서 모든 인터프리터가 하나의 GIL을 공유하는 기존(3.12 이전) 동작입니다. 이처럼 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이 미치는 영향에 관한 자세한 내용은 Extension Module Thread Safety를 참조하십시오.
호환되지 않는 확장 모듈을 가져왔고 현재 “PyInterpreterState.strict_extension_compat” 값이 true이면 가져오기 시스템에서 ImportError를 발생시킵니다. (false인 경우에는 단순히 검사하지 않습니다.) 이 작업은 importlib._bootstrap_external.ExtensionFileLoader를 통해 수행됩니다(실제로는 _imp.create_dynamic(), _PyImport_LoadDynamicModuleWithSpec(), PyModule_FromDefAndSpec2()를 통해 수행됩니다).
“PyInterpreterState.strict_extension_compat”은 두 경우 모두 false로 초기화되므로, 주 인터프리터(또는 Py_NewInterpreter()를 통해 생성된 인터프리터)에서는 이러한 가져오기가 절대 실패하지 않습니다. 따라서 레거시(3.12 이전) 동작이 보존됩니다.
널리 사용되는 확장 모듈이 여러 인터프리터에서 사용될 수 있도록 지원하는 작업을 해당 개발자들과 함께 진행할 예정입니다. 여기에는 CPython의 공개 C-API에 추가하는 작업이 포함될 수 있으며, 이 문제는 사례별로 다룰 예정입니다.
확장 모듈 호환성
Extension Modules에서 언급했듯이, 많은 확장 모듈은 변경이 필요하지 않은 상태에서도 여러 인터프리터에서(그리고 인터프리터별 GIL 아래에서도) 문제없이 작동합니다. 이러한 모듈이 지원을 명시적으로 나타내지 않으면 가져오기 시스템은 여전히 실패합니다. 처음에는 그렇게 하는 확장 모듈이 많지 않을 것이므로, 이는 불만을 일으킬 수 있는 잠재적인 원인입니다.
여러 인터프리터 지원 검사를 일시적으로 비활성화하는 컨텍스트 관리자를 추가하여 이 문제를 해결할 예정입니다: importlib.util.allow_all_extensions(). 대략적으로 말하면, 이는 현재의 “PyInterpreterState.strict_extension_compat” 값을 수정합니다(예: 비공개 sys 함수를 통해 수정합니다).
확장 모듈 스레드 안전성
모듈이 여러 인터프리터에서의 사용을 지원한다면, 이는 대체로 해당 인터프리터들이 GIL을 공유하지 않더라도 작동한다는 의미입니다. 한 가지 예외는 모듈이 스레드 안전하지 않은 내부 전역 상태를 가진 라이브러리에 링크하는 경우입니다. (임시 버퍼로 사용하는 정적 지역 변수처럼 무해해 보이는 것조차 문제가 될 수 있습니다.) GIL을 공유하면 해당 상태가 보호됩니다. 공유 GIL이 없으면 이러한 모듈은 해당 상태를 사용하는 모든 작업을 잠금으로 감싸야 합니다(예: 호출을 통해 감쌉니다).
현재 supports-multiple-interpreters(여러 인터프리터 지원)가 supports-per-interpreter-gil(인터프리터별 GIL 지원)과 충분히 동등하여 특별한 조치를 피할 수 있는지는 명확하지 않습니다. 이는 여전히 의미 있는 논의와 조사가 진행 중인 사안입니다. 두 기능의 실질적인 차이는(예를 들어 PyPI와 같은 Python 커뮤니티에서) 이 문제를 결정할 만큼 충분히 이해되지 않은 상태입니다. 마찬가지로, 이 문제가 실제로 존재한다고 가정할 때 확장 모듈 유지 관리자가 이를 완화하도록 지원하기 위해 무엇을 할 수 있을지도 명확하지 않습니다.
그동안에는 이러한 차이가 현재 존재하는 충분한 수의 확장 모듈에 문제를 일으킬 만큼 클 것이라고 가정하고 진행해야 합니다. 적용할 해결책은 다음과 같습니다:
- 인터프리터별 GIL에서 확장을 가져올 수 있음을 나타내는
PyModuleDef슬롯을 추가하십시오(즉, 옵트인하십시오) - 앞서 논의한 대로 해당 슬롯을 “호환 가능한” 확장의 정의에 포함되는 항목으로 추가하십시오
단점은 모듈 유지 관리자가 아무리 사소하더라도 추가 작업을 하지 않으면 인터프리터별 GIL을 활용할 수 있는 확장 모듈이 단 하나도 없다는 것입니다. 이는 Extension Module Compatibility에 설명된 문제를 악화시키며, 동일한 우회 방법이 적용됩니다. 이상적으로는 차이가 문제가 될 만큼 충분히 크지 않다는 것을 확인해야 합니다.
인터프리터별 GIL에서 가져오기하려면 옵트인이 필요하게 되었지만 나중에 그것이 필요하지 않다고 판단한다면, 그 시점에 기본값을 전환하고 기존 옵트인 슬롯을 아무 작업도 하지 않도록 만든 다음 명시적으로 옵트아웃하기 위한 새로운 PyModuleDef 슬롯을 추가할 수 있습니다. 실제로는 처음부터 해당 옵트아웃 슬롯을 추가하는 것이 타당합니다.
문서화
- C-API:
Doc/c-api/init.rst의 “서브인터프리터 지원” 섹션에서 업데이트된 API를 자세히 설명합니다. - C-API: 해당 섹션에서는 인터프리터별 GIL의 결과를 설명합니다.
- importlib:
ExtensionFileLoader항목에 서브인터프리터에서 가져오기가 실패할 수 있음을 명시합니다. - importlib:
importlib.util.allow_all_extensions()에 관한 새 항목이 추가됩니다.
영향
하위 호환성
이 제안으로 인해 동작이나 API가 변경되지는 않지만, 두 가지 예외가 있습니다.
- 일부 확장은 일부 서브인터프리터에서 가져오기에 실패합니다(the next section 참조).
- 현재 스레드로부터 안전하지 않은 “mem” 및 “object” 할당자는 여러 인터프리터와 함께 사용될 때 이제 데이터 경쟁의 영향을 받을 수 있습니다.
인터프리터를 관리하기 위한 기존 C-API는 현재 동작을 유지하며, 새로운 API를 통해 새로운 동작이 제공됩니다. 안정 ABI와의 호환성을 포함하여 다른 API나 런타임 동작은 변경되지 않습니다.
관련 논의는 아래의 Objects Exposed in the C-API 를 참조하십시오.
확장 모듈
현재 Python의 가장 일반적인 사용 방식은 단연 메인 인터프리터만 실행하는 것입니다. 이 제안은 그러한 시나리오의 확장 모듈에 전혀 영향을 주지 않습니다. 마찬가지로 기존의 Py_NewInterpreter()를 사용하여 만든 여러 인터프리터에서도 좋든 나쁘든 동작에는 변화가 없습니다.
일부 확장은 모듈 상태를 전역 변수에 저장하기 때문에(또는 internal state of linked libraries 때문이기 때문에) 여러 인터프리터에서 사용하면 이미 중단된다는 점을 염두에 두십시오. 확장이 충돌하거나, 더 나쁘게는 일관되지 않은 동작을 보일 수 있습니다. 이것이 PEP 630 및 관련 제안들의 동기 중 하나였으므로, 이는 새로운 상황도 아니며 이 제안의 결과도 아닙니다.
이와 달리 적절한 설정과 함께 proposed API를 사용하여 여러 인터프리터를 만들면 호환되지 않는 확장의 동작이 변경됩니다. 이 경우 Restricting Extension Modules에 설명된 대로 이러한 확장을 가져오면 메인 인터프리터 외부에서는 실패합니다. 여러 인터프리터에서 이미 중단되는 확장의 경우 이는 개선입니다.
또한 일부 확장 모듈은 스레드로부터 안전하지 않은 내부 전역 상태를 가진 라이브러리에 연결됩니다. (Extension Module Thread Safety 참조.) 이러한 모듈은 해당 상태를 직접 또는 간접적으로 사용하는 모든 부분을 잠금으로 감싸야 합니다. 이는 다중 단계 초기화도 구현하여 여러 인터프리터에 대한 지원, 즉 격리를 나타내는 다른 모듈과의 핵심적인 차이입니다.
이제 위에서 언급한 호환성 단절에 이르렀습니다. 일부 확장 기능은 여러 인터프리터(및 인터프리터별 GIL)에서 안전하지만, 그러한 사실을 나타내지는 않았습니다. 안타깝게도 가져오기 시스템이 그러한 확장 기능이 안전한지 추론할 신뢰할 수 있는 방법이 없으므로, 이를 가져오려 하면 여전히 실패합니다. 이 경우는 위의 Extension Module Compatibility에서 다룹니다.
확장 모듈 유지 관리자
이와 관련하여, 인터프리터별 GIL은 여러 인터프리터의 사용 증가를 촉진할 가능성이 높으며, 특히 PEP 554가 승인되면 더욱 그러합니다. 일부 대규모 확장 모듈 유지 관리자는 여러 인터프리터의 사용 증가로 인해 예상되는 부담 증가에 우려를 표명했습니다.
구체적으로 여러 인터프리터 지원을 활성화하려면 일부 확장 모듈(아마 많지는 않겠지만)에 상당한 작업이 필요합니다. 그러한 지원을 추가하려면 해당 모듈의 유지 관리자(대개 자원봉사자)가 평소의 우선순위와 관심사를 제쳐 두고 호환성에 집중해야 합니다(PEP 630 참조).
물론 확장 모듈 유지 관리자는 여러 인터프리터에서의 사용을 지원하지 않도록 선택할 자유가 있습니다. 그러나 특히 이 기능의 인기가 높아지면 사용자는 이러한 지원을 점점 더 요구할 것입니다.
어느 쪽이든 그러한 확장 기능의 유지 관리자에게는 상황이 스트레스가 될 수 있으며, 특히 여가 시간에 작업을 수행하는 경우에는 더욱 그렇습니다. 이들이 표명한 우려는 이해할 만하며, Restricting Extension Modules 및 Extension Module Compatibility 절에서 부분적인 해결책을 다룹니다.
대체 파이썬 구현
다른 파이썬 구현은 동일한 프로세스에서 여러 인터프리터를 지원할 필요가 없습니다(일부 구현은 이미 지원하지만).
보안 영향
이 제안이 보안에 미치는 영향은 알려진 바가 없습니다.
유지 관리성
한편으로 이 제안은 CPython을 더 유지 관리하기 쉽게 만드는 여러 개선을 이미 촉진했습니다. 이러한 추세는 계속될 것으로 예상됩니다. 다른 한편으로 기반 작업을 통해 런타임에 기존부터 존재했으며 수정해야 했던 다양한 결함이 이미 드러났습니다. 여러 인터프리터가 더 많이 사용됨에 따라 이러한 현상도 계속될 것으로 예상됩니다. 그 밖에는 유지 관리성에 미치는 영향이 크지 않을 것이므로, 순효과는 긍정적일 것입니다.
성능
전역 변수를 통합하는 작업은 이미 CPython의 성능을 개선하여 속도를 높이고 메모리를 덜 사용하도록 했으며, 이러한 개선은 계속될 것입니다. 인터프리터별 GIL의 성능상 이점은 구체적으로 연구되지 않았습니다. 최소한 인터프리터가 충분히 격리되어 있는 한 CPython을 느리게 하지는 않을 것으로 예상됩니다. 또한 분명히 파이썬 코드에서 다양한 멀티코어 병렬 처리가 가능해집니다.
이 내용을 가르치는 방법
관련 PEP 554와 달리, 이는 소수의 C-API 사용자를 위한 고급 기능입니다. API의 세부 사항이나 API의 직접적인 적용을 가르칠 것으로 예상하지 않습니다.
그렇다고 하더라도 이를 가르친다면 다음과 같이 요약될 것입니다:
Py_NewInterpreter() 외에도 Py_NewInterpreterFromConfig()를 사용하여 인터프리터를 생성할 수 있습니다. 전달하는 config는 해당 인터프리터가 어떻게 동작하기를 원하는지 나타냅니다.
또한, 격리된 인터프리터를 생성하는 모든 확장 모듈의 유지 관리자는 인터프리터별 GIL이 사용자에게 미치는 영향을 설명해야 할 가능성이 높습니다. 먼저 설명해야 할 것은 격리된 인터프리터가 가능하게 하는 동시성 모델에 대해 PEP 554가 알려 주는 내용입니다. 이는 해당 동시성 모델을 사용하여 작성된 Python 소프트웨어가 이제 멀티코어 병렬성을 활용할 수 있다는 점으로 이어지며, 멀티코어 병렬성은 현재 GIL에 의해 방해받고 있습니다.
참조 구현
<TBD>
미해결 문제
- “mem” 및 “object” 할당자가 스레드로부터 안전하도록 요구해도 괜찮습니까?
- 인터프리터별 tracemalloc 모듈은 전역 할당자와 어떤 관계를 맺게 됩니까?
- faulthandler 모듈은 (signal 모듈처럼) 주 인터프리터로 제한해야 합니까, 아니면 세분화된 잠금으로 보호하여 인터프리터 간에 해당 전역 상태가 누출되도록 해야 합니까?
- “Consolidating Runtime Global State” 섹션을 바탕으로 관련 정보를 모두 포함한 정보 제공용 PEP를 별도로 분리할까요?
- 모듈이 여러 인터프리터(격리)에서는 작동하지만 인터프리터별 GIL에서는 작동하지 않을 가능성은 얼마나 됩니까? (Extension Module Thread Safety를 참조하십시오.)
- 그 가능성이 충분히 높다면, 확장 모듈 유지 관리자들이 문제를 완화하고 인터프리터별 GIL에서 사용할 수 있도록 하려면 무엇을 할 수 있습니까?
allow_all_extensions에 더 나은 (더 무섭게 들리는) 이름은 무엇입니까?
연기된 기능
- 인터프리터를 항상 새 스레드에서 실행하도록 하는
PyInterpreterConfig옵션 - 인터프리터에 “main” 스레드를 할당하고 해당 스레드에서만 실행하도록 하는
PyInterpreterConfig옵션
거부된 아이디어
<TBD>
추가 맥락
전역 객체 공유
인터프리터 간에 일부 전역 객체를 공유합니다. 이는 구현 세부 사항이며 이 제안보다는 globals consolidation에 더 관련되지만, 여기에서 설명할 만큼 충분히 중요한 세부 사항입니다.
그 대안은 인터프리터 간에 객체를 절대로 공유하지 않는 것입니다. 이를 실현하려면 모든 정적 타입의 향방을 정리해야 하며, exposed in the public C-API 에 해당하는 많은 객체의 호환성 문제도 처리해야 합니다.
이 접근 방식은 상당한 추가 복잡성과 더 높은 위험을 초래하지만, 프로토타이핑을 통해 유효한 해결책이 입증되었습니다. 또한 성능 저하가 발생할 가능성이 높습니다.
Immortal objects를 사용하면 그 밖에는 변경 불가능한 전역 객체를 공유할 수 있습니다. 이를 통해 추가 비용을 피할 수 있습니다.
C-API에 노출된 객체
C-API(제한된 API 포함)는 내장 예외를 포함한 모든 내장 타입과 내장 싱글턴을 노출합니다. 예외는 PyObject *로 노출되지만 나머지는 포인터가 아니라 정적 값으로 노출됩니다. 이는 인터프리터별 GIL을 위해 해결해야 했던 몇 안 되는 쉽지 않은 문제 중 하나였습니다.
불멸 객체를 사용하면 이는 문제가 되지 않습니다.
런타임 전역 상태 통합
위의 CPython Runtime State에서 언급했듯이, (이 PEP와는 별개로) CPython의 전역 상태를 _PyRuntimeState 구조체로 통합하려는 작업이 진행 중입니다. 작업의 거의 전부는 해당 상태를 전역 변수에서 옮기는 일과 관련됩니다. 이 프로젝트는 이 제안과 특히 관련이 있으므로, 아래에 추가 세부 사항을 설명합니다.
통합의 이점
전역 변수를 통합하면 다음과 같은 다양한 이점이 있습니다:
- C 전역 변수의 수를 크게 줄입니다(C 코드의 모범 사례).
- 상태를 옮기는 과정에서 불안정하거나 손상된 런타임 상태에 주목하게 됩니다.
- 런타임 상태를 사용하는 방식의 일관성이 향상됩니다.
- CPython의 런타임 상태를 발견하고 식별하기가 쉬워집니다.
- 일관된 방식으로 런타임 상태를 정적으로 할당하기가 쉬워집니다.
- 런타임 상태의 메모리 지역성이 향상됩니다.
또한 위의 Indirect Benefits에 나열된 모든 이점이 여기에도 적용되며, 해당 항목에 나열된 동일한 프로젝트도 이점을 얻습니다.
작업 규모
옮겨야 할 전역 변수의 수는 무시할 수 없을 만큼 많지만, 대부분은 큰 그룹 단위로 처리할 수 있는 Python 객체입니다(예: Py_IDENTIFIER). 거의 모든 경우에 이러한 전역 변수를 인터프리터로 옮기는 작업은 매우 기계적입니다. 이는 기지를 필요로 하는 일이 아니라 누군가가 시간을 들여 수행해야 하는 일입니다.
옮길 상태
남은 전역 변수는 다음과 같이 분류할 수 있습니다:
- 전역 객체
- 정적 타입(예외 타입 포함)
- 비정적 타입(힙 타입 및 structseq 타입 포함)
- 싱글턴(정적)
- 싱글턴(한 번 초기화됨)
- 캐시된 객체
- 비객체
- 초기화 후 변경되지 않거나 변경될 가능성이 낮음
- 주 스레드에서만 사용됨
- 지연 초기화됨
- 사전 할당된 버퍼
- 상태
이러한 전역 변수는 핵심 런타임, 내장 모듈 및 표준 라이브러리 확장 모듈에 분산되어 있습니다.
나머지 전역 변수의 내역을 보려면 다음을 실행하십시오:
./python Tools/c-analyzer/table-file.py Tools/c-analyzer/cpython/globals-to-fix.tsv
이미 완료된 작업
앞서 언급했듯이 이 작업은 수년 동안 진행되어 왔습니다. 다음은 이미 완료된 작업 중 일부입니다:
도구
앞서 설명했듯이 전역 변수를 식별하고 이를 분석하는 데 도움이 되는 여러 도구가 있습니다.
Tools/c-analyzer/cpython/globals-to-fix.tsv- 남은 전역 변수 목록Tools/c-analyzer/c-analyzer.pyanalyze- 모든 전역 변수 식별check- 무시되지 않은 지원되지 않는 전역 변수가 있으면 실패
Tools/c-analyzer/table-file.py- 알려진 전역 변수 요약
또한 지원되지 않는 전역 변수 검사가 CI에 통합되어 있으므로 새로운 전역 변수가 실수로 추가되지 않습니다.
전역 객체
인터프리터 간에 (전역 인터프리터 잠금 없이) 안전하게 공유할 수 있는 전역 객체는 _PyRuntimeState에 유지할 수 있습니다. 객체는 사실상 불변이어야 할 뿐만 아니라(예: 싱글턴, 문자열), 안전하려면 참조 카운트조차 변경될 수 없어야 합니다. 불멸성(PEP 683)이 이를 제공합니다. (대안은 객체를 전혀 공유하지 않는 것이지만, 이는 특히 exposed in the public C-API객체의 경우 해결책에 상당한 복잡성을 추가합니다.)
내장 정적 타입은 공유될 전역 객체의 특수한 경우입니다. 이들은 한 부분인 __subclasses__ (AKA tp_subclasses)을 제외하면 사실상 불변입니다. __dict__의 내용(AKA tp_dict)조차도 내장 타입에서 다른 어떤 것도 변경되지 않을 것으로 예상합니다.
내장 타입의 __subclasses__는 해당 타입에 대해 현재 PyInterpreterState에서 조회를 수행하는 게터로 만들어 처리합니다.
참조
관련 항목:
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.