PEP 311 – 확장 기능을 위한 간소화된 전역 인터프리터 잠금 획득
- Author:
- Mark Hammond <mhammond at skippinet.com.au>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 05-Feb-2003
- Python-Version:
- 2.3
- Post-History:
- 05-Feb-2003, 14-Feb-2003, 19-Apr-2003
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 Python 확장 모듈에서 전역 인터프리터 잠금(GIL)에 액세스하기 위한 간소화된 API를 제안합니다. 구체적으로, Python의 현재 상태(즉, GIL의 상태)를 알 수 없는 복잡한 다중 스레드 확장 기능 작성자를 위한 해결책을 제공합니다.
이 PEP는 스레딩을 지원하도록 구축된 플랫폼에서 Python 스레드 상태를 관리하기 위한 새로운 API를 제안합니다. 초기 플랫폼 독립적 구현과 함께 구현 전략을 제안합니다.
근거
현재 Python 인터프리터 상태 API는 단순한 단일 스레드 확장 기능에 적합하지만, 사소하지 않은 다중 스레드 확장 기능에서는 빠르게 매우 복잡해집니다.
현재 Python은 GIL을 처리하기 위한 두 가지 메커니즘을 제공합니다.
Py_BEGIN_ALLOW_THREADS및Py_END_ALLOW_THREADS매크로입니다. 이러한 매크로는 주로 이미 GIL을 소유한 단순한 Python 확장 기능이 일반적으로 비용이 많이 드는 “외부”(즉, Python이 아닌) 호출을 수행하는 동안 GIL을 일시적으로 해제할 수 있도록 제공됩니다. 그러면 GIL을 기다리며 차단되어 있던 기존 Python 스레드가 실행될 수 있습니다. Python에서 외부 세계로 호출하는 확장 기능에는 문제가 없지만, 스레드 상태를 알 수 없는 상황에서 Python으로 호출해야 하는 확장 기능에는 아무런 도움이 되지 않습니다.PyThreadState및PyInterpreterStateAPI입니다. 이러한 API 함수는 확장 기능 또는 임베디드 애플리케이션이 GIL을 획득할 수 있도록 하지만, 사용하기 전에 Python 인터프리터와 GIL의 상태를 알고 있어야 한다는 심각한 부트스트래핑 문제를 겪습니다. 특히 Python이 이전에 본 적이 없는 스레드를 처리해야 하지만 해당 스레드에서 Python을 호출해야 하는 확장 기능 작성자에게 문제가 됩니다. 이러한 “새로운” 스레드가 항상 GIL의 정확한 상태를 알고 있어 이 API와 안정적으로 상호 작용하도록 확장 기능을 작성하는 일은 매우 어렵고 까다로우며 오류가 발생하기 쉽습니다.
이러한 이유로 그러한 확장 기능이 Python과 상호 작용해야 하는 방식에 대한 질문은 빠르게 FAQ가 되고 있습니다. 이 PEP의 주된 계기가 된 python-dev의 스레드 [1]는 이와 정확히 같은 문제를 가진 다음 프로젝트를 즉시 식별했습니다.
- win32all 확장 기능
- Boost
- ctypes
- Python-GTK 바인딩
- Uno
- PyObjC
- Mac 도구 상자
- PyXPCOM
현재 이 문제에 대한 합리적이고 이식 가능한 해결책이 없으므로, 각 확장 기능 작성자가 자체적으로 만든 버전을 구현해야 합니다. 더구나 이 문제는 복잡하므로 많은 구현이 잘못될 가능성이 높으며, 그 결과 다양한 문제가 발생하고 종종 단순히 “Python이 멈췄습니다”라는 형태로 나타납니다.
기존 스레드 상태 API의 가장 큰 문제는 잠금의 현재 상태를 조회할 수 없다는 점이지만, 확장 기능 작성자에게 더 완전하고 간소화된 해결책을 제공해야 한다고 여겨집니다. 이러한 해결책은 작성자가 Python의 스레딩 메커니즘을 최대한 활용하는 오류 없는 복잡한 확장 모듈을 제공하도록 장려해야 합니다.
제한 사항 및 제외 항목
이 제안은 복잡한 멀티스레드 요구사항을 가지고 있지만 단일 “PyInterpreterState”만 필요로 하는 확장 작성자를 위한 해결책을 제시합니다. 여러 인터프리터 상태를 필요로 하는 확장을 지원하려는 시도는 하지 않습니다. 작성 시점 기준으로, 여러 PyInterpreterState를 필요로 하는 확장은 확인된 바 없으며, 실제로 그 기능이 Python 자체에서 올바르게 작동하는지도 명확하지 않습니다.
이 API는 Python의 자동 초기화를 수행하지 않으며, 멀티스레드 동작을 위해 Python을 초기화하지도 않습니다. 확장 작성자는 계속해서 Py_Initialize()를 호출해야 하며, 멀티스레드 애플리케이션의 경우 PyEval_InitThreads()를 호출해야 합니다. 이는 PyEval_InitThreads()를 처음 호출하는 스레드가 Python에 의해 “메인 스레드”로 지정되기 때문이며, 따라서 확장 작성자에게 이 첫 호출을 하도록 요구함으로써 메인 스레드를 명시하도록 강제하는 것이 모호함을 제거합니다. Py_Initialize()는 PyEval_InitThreads()보다 먼저 호출되어야 하며, 이 두 함수 모두 현재 여러 번 호출되는 것을 지원하므로, 이것이 확장 작성자에게 부과하는 부담은 합리적이라고 간주됩니다.
이 API는 Python GIL을 획득하는 데 필요한 전부가 되도록 의도되었습니다. 기존의 표준 Py_BEGIN_ALLOW_THREADS 및 Py_END_ALLOW_THREADS 매크로를 제외하면, 확장에서 추가적인 스레드 상태 API 함수를 사용하지 않을 것으로 가정합니다. 이러한 복잡한 요구사항을 가진 확장은 기존 스레드 상태 API를 계속 자유롭게 사용할 수 있습니다.
제안
이 제안은 GIL 관리를 단순화하기 위해 Python에 새로운 API를 추가할 것을 권장합니다. 이 API는 WITH_THREAD가 정의된 상태로 빌드된 모든 플랫폼에서 사용할 수 있습니다.
의도는, Python이 올바르게 초기화되었다고 가정할 때, 확장 작성자가 언제든지 어떤 스레드에서든 작고 명확하게 정의된 “서두 절차(prologue dance)”를 사용하여 해당 스레드에서 Python을 사용할 준비가 되도록 보장할 수 있게 하는 것입니다. 확장이 Python 사용을 마친 후에는, 이전에 획득한 자원을 해제하기 위해 “결말 절차(epilogue dance)”도 수행해야 합니다. 이상적으로는 이러한 절차들을 한 줄로 표현할 수 있습니다.
구체적으로, 다음과 같은 새로운 API가 제안됩니다:
/* Ensure that the current thread is ready to call the Python
C API, regardless of the current state of Python, or of its
thread lock. This may be called as many times as desired
by a thread so long as each call is matched with a call to
PyGILState_Release(). In general, other thread-state APIs may
be used between _Ensure() and _Release() calls, so long as the
thread-state is restored to its previous state before the Release().
For example, normal use of the Py_BEGIN_ALLOW_THREADS/
Py_END_ALLOW_THREADS macros are acceptable.
The return value is an opaque "handle" to the thread state when
PyGILState_Acquire() was called, and must be passed to
PyGILState_Release() to ensure Python is left in the same state. Even
though recursive calls are allowed, these handles can *not* be
shared - each unique call to PyGILState_Ensure must save the handle
for its call to PyGILState_Release.
When the function returns, the current thread will hold the GIL.
Failure is a fatal error.
*/
PyAPI_FUNC(PyGILState_STATE) PyGILState_Ensure(void);
/* Release any resources previously acquired. After this call, Python's
state will be the same as it was prior to the corresponding
PyGILState_Acquire call (but generally this state will be unknown to
the caller, hence the use of the GILState API.)
Every call to PyGILState_Ensure must be matched by a call to
PyGILState_Release on the same thread.
*/
PyAPI_FUNC(void) PyGILState_Release(PyGILState_STATE);
일반적인 사용법은 다음과 같습니다:
void SomeCFunction(void)
{
/* ensure we hold the lock */
PyGILState_STATE state = PyGILState_Ensure();
/* Use the Python API */
...
/* Restore the state of Python */
PyGILState_Release(state);
}
설계와 구현
PyGILState_Ensure()의 일반적인 동작은 다음과 같습니다:
- Python이 초기화되었는지 단언합니다.
- 현재 스레드에 대한
PyThreadState를 가져오며, 필요하면 생성하고 저장합니다. - 잠금의 현재 상태(소유/비소유)를 기억합니다
- 현재 상태가 GIL을 소유하고 있지 않다면 이를 획득합니다.
- 현재 스레드에서
PyGILState_Ensure가 호출된 횟수를 세는 카운터를 증가시킵니다. - 반환합니다
PyGILState_Release()의 일반적인 동작은 다음과 같습니다:
- 현재 스레드가 잠금을 보유하고 있는지 단언합니다.
- 이전 상태가 잠금이 이전에 잠기지 않았음을 나타낸다면 GIL을 해제합니다.
- 해당 스레드의
PyGILState_Ensure카운터를 감소시킵니다. - 카운터 == 0이면:
PyThreadState를 해제하고 삭제합니다.ThreadState를 해당 스레드가 소유하고 있다는 사실을 잊습니다.
- 반환합니다.
하나의 스레드에 대해 서로 다른 두 개의 PyThreadStates가 사용되면 오류인 것으로 가정합니다. pystate.h의 주석(“스레드별로 고유한 상태”)은 이러한 견해를 뒷받침하지만, 직접적으로 명시된 적은 없습니다. 따라서 이는 스레드 로컬 저장소(Thread Local Storage)의 어떤 구현을 필요로 합니다. 다행히도, 플랫폼 독립적인 스레드 로컬 저장소 구현이 Python 소스 트리 내 SGI 스레딩 포트에 이미 존재합니다. 이 코드는 플랫폼 독립적인 Python 코어에 통합될 예정이지만, 원한다면 각 플랫폼이 더 최적화된 구현을 제공할 수 있는 방식으로 통합될 것입니다.
구현
이 제안에 대한 구현은 https://bugs.python.org/issue684256 에서 찾을 수 있습니다.
참고 자료
Copyright
This document has been placed in the public domain.