[Final] PEP 684 - A Per-Interpreter GIL
원문 링크: PEP 684 - A Per-Interpreter GIL
상태: Final 유형: Standards Track 작성일: 08-Mar-2022
PEP 684 – 인터프리터별 GIL (A Per-Interpreter GIL)
- 작성자: Eric Snow
- 상태: Final (최종)
- 유형: Standards Track (표준 트랙)
- Python 버전: 3.12
- 생성일: 2022년 3월 8일
개요 (Abstract)
Python 1.5 (1997년) 이후로 CPython 사용자들은 동일 프로세스 내에서 여러 인터프리터를 실행할 수 있었지만, 이 인터프리터들은 상당한 양의 전역 상태(global state)를 공유했습니다. 이는 버그의 원인이 되며, 이 기능의 사용이 증가함에 따라 그 영향도 커지고 있습니다. 또한, 충분한 격리가 이루어진다면 인터프리터들이 더 이상 GIL을 공유하지 않아 진정한 멀티 코어 병렬 처리가 가능해질 것입니다. 이 제안에 설명된 변경 사항들은 이러한 수준의 인터프리터 격리를 가능하게 할 것입니다.
상위 수준 요약 (High-Level Summary)
이 제안은 CPython을 다음과 같이 변경합니다.
- 충분한 격리가 이루어지면 인터프리터 간의 GIL 공유를 중단합니다.
- 격리 설정에 대한 몇 가지 새로운 인터프리터 설정 옵션을 추가합니다.
- 호환되지 않는 확장 모듈이 문제를 일으키는 것을 방지합니다.
GIL (The GIL)
GIL은 CPython 런타임 상태의 대부분에 대한 동시 접근을 보호합니다. 따라서 GIL로 보호되는 모든 전역 상태는 GIL이 인터프리터별로 분리되기 전에 각 인터프리터로 이동되어야 합니다. 일부 경우에는 잠금(locks)이나 “불멸 객체(immortal objects)”와 같은 다른 메커니즘을 사용하여 스레드 안전한 공유를 보장할 수 있습니다.
CPython 런타임 상태 (CPython Runtime State)
인터프리터를 제대로 격리하려면 CPython 런타임 상태의 대부분이 PyInterpreterState
구조체에 저장되어야 합니다. 현재는 일부만 저장되어 있고, 나머지는 C 전역 변수 또는 _PyRuntimeState
에 있습니다. 이 대부분의 상태는 이동되어야 합니다. 이는 전역 변수의 내부 사용을 크게 줄이고 런타임 상태를 _PyRuntimeState
와 PyInterpreterState
로 통합하려는 오랜 노력과 직접적으로 일치합니다.
기타 격리 고려 사항 (Other Isolation Considerations)
CPython의 인터프리터는 몇 가지 예외를 제외하고는 서로 엄격하게 격리되어야 합니다. 대부분은 이미 격리되어 있으며, 각 인터프리터는 자신만의 모듈, 클래스, 함수, 변수 복사본을 가집니다.
그러나 일부 프로세스 전역 리소스(예: 메모리, 파일 디스크립터, 환경 변수)는 여전히 공유됩니다. 이를 변경할 계획은 없습니다. 또한, 버그나 다중 인터프리터를 고려하지 않은 구현으로 인해 격리가 불완전한 경우도 있습니다. 여기에는 CPython 런타임, 표준 라이브러리(stdlib), 그리고 전역 변수에 의존하는 확장 모듈이 포함됩니다.
불멸 객체에 대한 의존성 (Depending on Immortal Objects)
PEP 683은 불멸 객체(Immortal Objects)를 CPython의 내부 기능으로 도입합니다. 불멸 객체를 통해 모든 인터프리터 간에 변경 불가능한 전역 객체를 공유할 수 있습니다. 결과적으로 이 PEP는 공개 C-API에 노출된 다양한 객체들을 다룰 필요가 없으며, 내장 정적 타입(builtin static types)에 대한 문제도 단순화합니다.
동기 (Motivation)
이 제안이 해결하고자 하는 근본적인 문제는 CPython 런타임에서 (Python 코드에 대한) 진정한 멀티 코어 병렬 처리의 부족입니다. GIL이 그 원인입니다. 격리된 인터프리터는 특정 동시성 모델을 지원하는 효과적인 메커니즘이기도 합니다. PEP 554에서 더 자세히 논의됩니다.
간접적인 이점 (Indirect Benefits)
인터프리터별 GIL에 필요한 대부분의 노력은 다음과 같은 이점을 제공하며, 이는 어쨌든 수행할 가치가 있는 작업입니다.
- 다중 인터프리터 동작을 더욱 신뢰할 수 있게 만듭니다.
- 그동안 우선순위가 아니었던 오랜 런타임 버그들을 수정하게 만들었습니다.
- 이전에 알려지지 않았던 런타임 버그들을 노출하고 수정하도록 영감을 주었습니다.
- 더 깔끔한 런타임 초기화(PEP 432, PEP 587)를 이끌었습니다.
- 더 깔끔하고 완전한 런타임 종료를 이끌었습니다.
- C-API의 구조적 계층화(예:
Include/internal
)를 가져왔습니다. - 다른 CPython 관련 프로젝트(예: 성능 개선, pre-fork 애플리케이션 배포, 확장 모듈 격리, CPython 임베딩)에도 이점을 제공합니다.
다중 인터프리터의 기존 사용 (Existing Use of Multiple Interpreters)
다중 인터프리터를 위한 C-API는 수년 동안 사용되어 왔습니다. 최근까지 이 기능은 널리 알려지거나 광범위하게 사용되지 않았지만(mod_wsgi 제외), 지난 몇 년 동안 사용이 증가하고 있습니다. 현재 이 기능을 사용하는 공개 프로젝트로는 mod_wsgi, OpenStack, Ceph, JEP, Kodi 등이 있습니다. PEP 554가 채택되면 다중 인터프리터 사용은 상당히 증가할 것으로 예상됩니다.
PEP 554 (Stdlib의 다중 인터프리터) (Multiple Interpreters in the Stdlib)
PEP 554는 사용자가 Python 코드에서 다중 인터프리터에 접근할 수 있도록 최소한의 표준 라이브러리 모듈을 제공하는 것에 중점을 둡니다. 이 PEP는 GIL과 관련된 변경을 제안하지 않지만, 이 모듈의 사용자들은 인터프리터별 GIL의 이점을 누릴 수 있으므로 PEP 554의 매력이 더욱 커집니다.
근거 (Rationale)
2014년 초기 조사에서 멀티 코어 Python을 위한 다양한 가능한 해결책들이 탐색되었지만, 각각 간단한 해결책 없이 단점을 가지고 있었습니다.
- 확장 모듈에서 GIL 해제: Python 코드에는 도움이 되지 않습니다.
- 다른 Python 구현 (예: Jython, IronPython): CPython이 커뮤니티를 지배합니다.
- GIL 제거 (예: gilectomy, “no-gil”): 당시에는 너무 많은 기술적 위험이 있었습니다.
- Trent Nelson의 “PyParallel” 프로젝트: 불완전했으며, 당시 Windows 전용이었습니다.
- multiprocessing: 효과적으로 만들기에는 너무 많은 작업이 필요하고, 특정 상황(대규모, Windows)에서 높은 패널티가 따릅니다.
- 다른 병렬 처리 도구 (예: dask, ray, MPI): 런타임/stdlib에는 적합하지 않습니다.
- 멀티 코어를 포기 (예: async, 아무것도 하지 않음): 이는 결국 좋지 않은 결과를 낳을 뿐입니다.
2014년에도 격리된 인터프리터를 사용하는 해결책은 기술적 위험이 높지 않고 대부분의 작업이 어쨌든 수행할 가치가 있다는 것이 상당히 분명했습니다. (단점은 해야 할 작업의 양이었습니다.)
명세 (Specification)
이 제안은 다음과 같은 변경 사항을 포함하며, 순서대로 진행되어야 합니다.
- 전역 런타임 상태(객체 포함)를
_PyRuntimeState
로 통합합니다. - 거의 모든 상태를
PyInterpreterState
로 이동합니다. - 마지막으로, GIL을
PyInterpreterState
로 이동합니다. - 기타 나머지 작업: C-API 업데이트, 확장 모듈 제한 구현, 인기 확장 모듈 유지 관리자와 협력하여 다중 인터프리터 지원을 돕습니다.
인터프리터별 상태 (Per-Interpreter State)
다음 런타임 상태는 PyInterpreterState
로 이동됩니다.
- 안전하게 공유할 수 없는(완전히 변경 불가능한) 모든 전역 객체
- GIL
- 현재 GIL로 보호되는 대부분의 변경 가능한 데이터
- 다른 인터프리터별 잠금으로 보호되는 변경 가능한 데이터
- 다른 인터프리터에서 독립적으로 사용될 수 있는 변경 가능한 데이터 (다단계 초기화를 포함하는 확장 모듈에도 적용)
- 아래에 명시적으로 제외되지 않은 모든 다른 변경 가능한 데이터
또한, GC, 경고, atexit
훅(hooks)을 포함한 전역 상태의 일부는 이미 인터프리터로 이동되었습니다.
다음 런타임 상태는 이동되지 않습니다.
- 안전하게 공유 가능한 전역 객체 (있다면)
- 변경 불가능한 데이터 (대부분
const
) - 사실상 변경 불가능한 데이터 (예: 초기화 후 변경되지 않는 상태, 문자열 해시)
- 메인 스레드에서만 변경이 보장되는 모든 데이터 (예: CPython의
main()
에서만 사용되는 상태, REPL 상태, 런타임 초기화 중에만 수정되는 데이터) - (GIL 외의) 다른 전역 잠금으로 보호되는 변경 가능한 데이터
- 원자 변수(atomic variables)의 전역 상태
- (합리적으로) 원자 변수로 변경될 수 있는 변경 가능한 전역 상태
메모리 할당자 (Memory Allocators)
이는 인터프리터를 격리하는 작업에서 가장 민감한 부분 중 하나입니다. 가장 간단한 해결책은 내부 “작은 블록(small block)” 할당자의 전역 상태를 PyInterpreterState
로 이동하는 것입니다.
CPython은 “raw”, “mem”, “object” 세 가지 할당자 도메인을 가진 메모리 관리 C-API를 제공합니다. 이들은 현재 런타임 전역이며 모든 인터프리터에 의해 공유됩니다. “mem” 및 “object” 할당자는 스레드 안전하지 않을 수 있으며 현재 스레드 안전성을 위해 GIL에 의존할 수 있습니다.
궁극적으로 다음의 가장 간단한 옵션을 선택할 것입니다.
- 할당자를 전역 런타임 상태에 유지합니다.
- 스레드 안전해야 한다고 요구합니다.
- 기본 객체 할당자(“작은 블록” 할당자)의 상태를
PyInterpreterState
로 이동합니다.
C-API
다음 API는 공개될 것입니다.
PyInterpreterConfig
(구조체)PyInterpreterConfig_INIT
(매크로)PyInterpreterConfig_LEGACY_INIT
(매크로)PyThreadState * Py_NewInterpreterFromConfig(PyInterpreterConfig *)
PyInterpreterConfig
에 두 가지 새로운 필드가 추가됩니다.
int own_gil
: 이 값이 참(1)이면 새 인터프리터는 자체 “전역” 인터프리터 잠금을 가집니다. 이는 새 인터프리터가 다른 인터프리터의 방해 없이 실행될 수 있음을 의미하며, 멀티 코어의 완전한 사용을 효과적으로 가능하게 합니다. 이 값이 거짓(0)이면 새 인터프리터는 메인 인터프리터의 잠금을 사용하며, 이는 CPython의 레거시(3.12 이전) 동작입니다.int strict_extensions_compat
:PyInterpreterState.strict_extension_compat
의 초기 값으로 사용됩니다.
PyInterpreterConfig_INIT
는 격리된 인터프리터를 얻는 데 사용되며, own_gil
은 true
로 설정됩니다. PyInterpreterConfig_LEGACY_INIT
는 기존 동작을 유지하며 own_gil
은 false
로 설정됩니다.
확장 모듈 제한 (Restricting Extension Modules)
확장 모듈은 전역 변수에 상태가 저장될 때 런타임과 동일한 많은 문제를 가집니다. PEP 630은 격리를 지원하고 여러 인터프리터에서 동시에 안전하게 실행하기 위해 확장 모듈이 수행해야 하는 모든 세부 사항을 다룹니다.
확장 모듈이 다단계 초기화(multi-phase init, PEP 489 참조)를 구현하면 다중 인터프리터와 호환되는 것으로 간주됩니다. 그렇지 않은 모든 확장 모듈은 비호환으로 간주됩니다.
비호환 확장 모듈이 임포트되고 현재 PyInterpreterState.strict_extension_compat
값이 true
이면 임포트 시스템은 ImportError
를 발생시킵니다. 메인 인터프리터에서는 이 값이 false
로 초기화되므로 임포트 실패가 발생하지 않습니다.
확장 모듈 호환성 (Extension Module Compatibility)
많은 확장 모듈은 변경 없이도 다중 인터프리터(및 인터프리터별 GIL)에서 잘 작동하지만, 명시적으로 지원을 표시하지 않으면 임포트 시스템이 실패할 수 있습니다. 이를 해결하기 위해 importlib.util.allow_all_extensions()
라는 컨텍스트 관리자를 추가하여 다중 인터프리터 지원 검사를 일시적으로 비활성화할 것입니다.
확장 모듈 스레드 안전성 (Extension Module Thread Safety)
모듈이 다중 인터프리터 사용을 지원한다는 것은, 인터프리터들이 GIL을 공유하지 않더라도 작동한다는 것을 의미합니다. 한 가지 주의할 점은 모듈이 내부 전역 상태가 스레드 안전하지 않은 라이브러리에 링크될 때입니다. 공유 GIL이 있을 때는 이 상태가 보호되지만, GIL이 없을 때는 그러한 모듈이 잠금(lock)을 사용하여 해당 상태의 모든 사용을 감싸야 합니다.
현재, “다중 인터프리터 지원”과 “인터프리터별 GIL 지원”이 충분히 동일한지 명확하지 않습니다. 따라서 추가적인 조치가 필요할 수 있습니다. 잠정적인 해결책으로, 확장 모듈이 인터프리터별 GIL 하에 임포트될 수 있음을 나타내는 PyModuleDef
슬롯(slot)을 추가하는 것을 고려하고 있습니다.
문서화 (Documentation)
- C-API:
Doc/c-api/init.rst
의 “Sub-interpreter support” 섹션에서 업데이트된 API와 인터프리터별 GIL의 결과를 상세히 설명할 것입니다. importlib
:ExtensionFileLoader
항목은 서브 인터프리터에서 임포트가 실패할 수 있음을 명시하고,importlib.util.allow_all_extensions()
에 대한 새로운 항목이 추가될 것입니다.
영향 (Impact)
하위 호환성 (Backwards Compatibility)
이 제안으로 인해 변경될 의도적인 동작이나 API는 두 가지 예외를 제외하고는 없습니다.
- 일부 확장 모듈은 특정 서브 인터프리터에서 임포트가 실패할 것입니다.
- 현재 스레드 안전하지 않은 “mem” 및 “object” 할당자는 다중 인터프리터와 함께 사용될 때 데이터 경쟁(data races)에 취약해질 수 있습니다.
기존 인터프리터 관리 C-API는 현재 동작을 유지하며, 새로운 동작은 새로운 API를 통해 노출됩니다.
확장 모듈 (Extension Modules)
가장 일반적인 Python 사용 시나리오(메인 인터프리터 단독 실행)에서는 이 제안이 확장 모듈에 미치는 영향은 없습니다. 기존 Py_NewInterpreter()
를 사용하여 생성된 다중 인터프리터에서도 동작에는 변화가 없습니다.
일부 확장 모듈은 전역 변수에 모듈 상태를 유지하거나 링크된 라이브러리의 내부 상태로 인해 이미 다중 인터프리터에서 오작동하는 경우가 있습니다. 제안된 API를 적절한 설정으로 사용하여 다중 인터프리터를 생성하는 경우, 비호환 확장 모듈의 동작이 변경될 것입니다. 이러한 확장을 임포트하면 (메인 인터프리터 외부에서는) 실패할 것입니다.
또한, 일부 확장 모듈은 스레드 안전하지 않은 내부 전역 상태를 가진 라이브러리에 링크됩니다. 이러한 모듈은 해당 상태의 직접 또는 간접적인 사용을 잠금으로 감싸야 할 것입니다.
확장 모듈 유지 관리자 (Extension Module Maintainers)
인터프리터별 GIL은 다중 인터프리터 사용의 증가를 촉진할 가능성이 높습니다. 일부 대규모 확장 모듈의 유지 관리자들은 이러한 증가로 인한 부담에 대해 우려를 표명했습니다. 다중 인터프리터 지원을 추가하는 것은 일부 확장 모듈에 상당한 작업을 요구할 수 있습니다.
확장 유지 관리자는 다중 인터프리터 사용에 대한 지원을 추가하지 않을 자유가 있지만, 기능의 인기가 높아지면 사용자들이 그러한 지원을 점점 더 요구할 것입니다.
대체 Python 구현 (Alternate Python Implementations)
다른 Python 구현은 동일 프로세스 내에서 다중 인터프리터 지원을 제공할 의무는 없습니다.
보안 영향 (Security Implications)
이 제안으로 인한 알려진 보안 영향은 없습니다.
유지 보수성 (Maintainability)
이 제안은 CPython의 유지 보수성을 향상시키는 여러 개선 사항을 이미 촉진했으며, 이는 계속될 것으로 예상됩니다. 또한, 기존 런타임 결함이 노출되어 수정되었으며, 다중 인터프리터 사용이 증가함에 따라 이러한 경향은 계속될 것입니다. 전반적으로 유지 보수성에 긍정적인 영향을 미칠 것으로 예상됩니다.
성능 (Performance)
전역 변수를 통합하는 작업은 이미 CPython의 성능(속도 향상 및 메모리 사용량 감소)에 여러 개선 사항을 제공했으며, 이는 계속될 것입니다. 인터프리터별 GIL 자체의 성능 이점은 아직 탐색되지 않았지만, CPython을 느리게 만들 것으로 예상되지는 않습니다. 그리고 분명히, Python 코드에서 다양한 멀티 코어 병렬 처리를 가능하게 할 것입니다.
교육 방법 (How to Teach This)
PEP 554와 달리, 이 기능은 C-API의 특정 사용자층을 위한 고급 기능입니다. API의 세부 사항이나 직접적인 적용 방법을 가르칠 필요는 없습니다.
다만, 교육이 필요하다면 다음과 같이 요약할 수 있습니다.
“Py_NewInterpreter() 외에도, Py_NewInterpreterFromConfig()를 사용하여 인터프리터를 생성할 수 있습니다. 전달하는 설정은 해당 인터프리터가 어떻게 동작하기를 원하는지 나타냅니다.”
또한, 격리된 인터프리터를 생성하는 확장 모듈의 유지 관리자는 사용자에게 인터프리터별 GIL의 결과를 설명해야 할 것입니다.
추가 컨텍스트 (Extra Context)
전역 객체 공유 (Sharing Global Objects)
일부 전역 객체는 인터프리터 간에 공유됩니다. 이는 구현 세부 사항이지만, 불멸 객체(PEP 683)를 통해 변경 불가능한 전역 객체를 공유함으로써 추가적인 복잡성과 성능 저하를 피할 수 있습니다.
C-API에 노출된 객체 (Objects Exposed in the C-API)
C-API는 모든 내장 타입(builtin types), 내장 예외(builtin exceptions) 및 내장 싱글톤(builtin singletons)을 노출합니다. 불멸 객체가 도입되면서 이는 더 이상 문제가 되지 않습니다.
런타임 전역 상태 통합 (Consolidating Runtime Global State)
CPython의 전역 상태를 _PyRuntimeState
구조체로 통합하려는 활발한 노력이 진행 중입니다. 이 프로젝트는 C 전역 변수에서 상태를 이동하는 것을 목표로 합니다.
통합의 이점 (Benefits to Consolidation)
- C 전역 변수의 수를 크게 줄입니다.
- 불안정하거나 깨진 런타임 상태에 주의를 기울이게 합니다.
- 런타임 상태 사용 방식의 일관성을 촉진합니다.
- CPython의 런타임 상태를 더 쉽게 발견/식별할 수 있습니다.
- 런타임 상태를 일관된 방식으로 정적으로 할당하기 쉽게 만듭니다.
- 런타임 상태에 대한 더 나은 메모리 지역성(locality)을 제공합니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments