Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 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와 PyInterpreterState API. 이 API 함수들을 사용하면 확장 모듈이나 임베딩 애플리케이션이 GIL을 획득할 수 있지만, 심각한 부트스트래핑 문제가 있습니다. 즉, 이 함수들을 사용하기 전에 Python 인터프리터의 상태와 GIL의 상태를 알고 있어야 합니다. 특히 문제가 되는 경우는 Python이 한 번도 본 적 없는 스레드를 다루면서도 그 스레드에서 Python을 호출해야 하는 확장 모듈 작성자입니다. 이러한 “새” 스레드가 항상 GIL의 정확한 상태를 알고 있어서 이 API와 안정적으로 상호 작용할 수 있도록 확장 모듈을 작성하는 일은 매우 어렵고, 까다로우며, 오류가 발생하기 쉽습니다.

이러한 이유로, 그러한 확장 모듈이 Python과 어떻게 상호 작용해야 하는지는 빠르게 자주 묻는 질문(FAQ)이 되어 가고 있습니다. 이 PEP의 주된 계기가 된 python-dev의 스레드 [1]에서는 바로 이 문제를 겪고 있는 다음 프로젝트들이 즉시 확인되었습니다:

  • win32all 확장 모듈
  • Boost
  • ctypes
  • Python-GTK 바인딩
  • Uno
  • PyObjC
  • Mac toolbox
  • 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을 소유하고 있지 않으면 GIL을 획득합니다.
  • 현재 스레드에서 PyGILState_Ensure가 몇 번 호출되었는지 세는 카운터를 증가시킵니다.
  • 반환합니다

PyGILState_Release()의 일반적인 동작은 다음과 같습니다:

  • 현재 스레드가 잠금을 보유하고 있는지 단언합니다.
  • 이전 상태가 잠금이 해제되어 있었음을 나타내면 GIL을 해제합니다.
  • 해당 스레드의 PyGILState_Ensure 카운터를 감소시킵니다.
  • 카운터 == 0이면:
    • PyThreadState를 해제하고 삭제합니다.
    • 스레드가 ThreadState를 소유하고 있다는 사실을 잊습니다.
  • return

하나의 스레드에서 서로 다른 두 PyThreadStates가 사용되면 오류라고 가정합니다. pystate.h의 주석(“State unique per thread”)은 직접 명시하지는 않지만 이러한 견해를 뒷받침합니다. 따라서 이를 위해서는 스레드 로컬 저장소(Thread Local Storage)를 어떤 식으로든 구현해야 합니다. 다행히 플랫폼에 독립적인 스레드 로컬 저장소 구현이 Python 소스 트리의 SGI 스레딩 포트에 이미 존재합니다. 이 코드는 플랫폼에 독립적인 Python 코어에 통합되지만, 원하는 경우 각 플랫폼이 더 최적화된 구현을 제공할 수 있는 방식으로 통합됩니다.

구현

이 제안의 구현은 https://bugs.python.org/issue684256 에서 찾을 수 있습니다.

참고 문헌