PEP 788 – 인터프리터 종료 처리로부터 C API 보호
- Author:
- Peter Bierma <peter at python.org>
- Sponsor:
- Victor Stinner <vstinner at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Type:
- Standards Track
- Created:
- 23-Apr-2025
- Python-Version:
- 3.15
- Post-History:
- 10-Mar-2025, 27-Apr-2025, 28-May-2025, 03-Oct-2025
- Resolution:
- 28-Apr-2026
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 종료 처리를 방지하여 인터프리터에 안전하게 연결할 수 있도록 C API에 함수 모음을 도입합니다. 구체적으로는 다음과 같습니다.
PyInterpreterGuard는 인터프리터의 종료 처리를 방지합니다.PyInterpreterView는 attached thread state를 보유하지 않고도 스레드 안전하게 인터프리터를 가져오는 방법을 제공합니다.PyThreadState_Ensure(),PyThreadState_EnsureFromView(),PyThreadState_Release()는 임의의 네이티브 코드에서 연결된 스레드 상태를 가져오기 위한 고수준 API입니다. 이는PyGILState_Ensure()및PyGILState_Release()와 유사합니다.
예를 들면 다음과 같습니다.
static int
thread_function(PyInterpreterView *view)
{
// Similar to PyGILState_Ensure(), but we can be sure that the interpreter
// is alive and well before attaching.
PyThreadStateToken *token = PyThreadState_EnsureFromView(view);
if (token == NULL) {
return -1;
}
// Now we can call Python code, without worrying about the thread
// hanging due to finalization.
if (PyRun_SimpleString("print('My hovercraft is full of eels')") < 0) {
PyErr_Print();
}
// Destroy the thread state and allow the interpreter to finalize.
PyThreadState_Release(token);
return 0;
}
용어
이 PEP에서 “종료 처리”라는 용어는 전체 Python 런타임이 아니라 단일 인터프리터의 종료 처리를 가리킵니다.
또한 이 PEP에서 “외부 스레드”라는 용어는 threading 모듈이 생성하지 않은 스레드를 가리킵니다. threading 모듈이 생성한 스레드는 이 제안에서 때때로 “Python 스레드”라고 부릅니다.
동기
인터프리터 종료 처리 중 외부 스레드가 멈춤
많은 대규모 라이브러리는 원하는 인터프리터가 종료 처리 중이거나 이미 종료된 매우 비동기적인 상황에서 Python 코드를 호출해야 할 수 있지만, 인터프리터를 호출한 후에도 코드를 계속 실행하려고 합니다. 이러한 요구는 사용자들이 brought up by users에서 제기했습니다. 예를 들어 Python 코드를 호출하려는 콜백은 다음과 같은 경우에 호출될 수 있습니다.
- GPU에서 커널 실행이 완료된 경우입니다.
- 네트워크 패킷을 수신한 경우입니다.
- 스레드가 종료되고 네이티브 라이브러리가 스레드 로컬 저장소의 정적 종료 처리자를 실행 중인 경우입니다.
일반적으로 이 패턴은 다음과 같이 나타납니다.
static void
some_callback(void *arg)
{
/* Do some work */
/* ... */
PyGILState_STATE gstate = PyGILState_Ensure();
/* Invoke the C API to do some computation */
PyGILState_Release(gstate);
/* ... */
}
여기에는 숨겨진 문제가 있습니다. 대상 인터프리터가 종료 처리 중이면 현재 스레드가 멈춥니다! 또는 대상 인터프리터가 완전히 삭제된 경우(수명이 짧은 서브인터프리터에서처럼)에는 연결 시 충돌이 발생할 가능성이 높습니다.
현재 이를 우회하는 방법은 몇 가지가 있습니다.
- Python을 호출할 필요가 없도록 리소스를 누수시킵니다.
- 최종화로부터 보호하려면
atexit콜백을 사용하십시오.
이러한 방법은 일반적으로 작동하지만 장황하거나 복잡할 수 있으며 다른 문제를 일으킬 수 있습니다. 이상적으로는 Python의 C API를 사용할 때 인터프리터 종료 처리가 이처럼 문제를 일으키는 함정이 되어서는 안 됩니다.
네이티브 확장의 잠금은 종료 처리 중에 사용할 수 없게 될 수 있습니다.
네이티브 API에서 잠금을 획득할 때는 잠금 획득 중 다른 코드가 실행될 수 있도록 GIL(또는 프리 스레드 빌드의 임계 구역)을 해제하는 것이 일반적이며, 종종 필요하기도 합니다. 잠금을 보유한 스레드가 멈출 수 있으므로 이는 종료 처리 중에 문제가 될 수 있습니다. 예를 들면 다음과 같습니다.
- 한 스레드가 교착 상태를 방지하기 위해 먼저 스레드 상태를 분리한 후 잠금을 획득하려고 합니다. 이는 일반적으로
Py_BEGIN_ALLOW_THREADS를 통해 수행합니다. - 주 스레드가 종료 처리를 시작하고 모든 스레드 상태에 연결 시 멈추도록 지시합니다.
- 해당 스레드는 기다리던 잠금을 획득하지만,
Py_END_ALLOW_THREADS를 통해 스레드 상태를 다시 연결하려고 시도하는 동안 멈춥니다. - 잠금을 보유한 스레드가 멈췄으므로 주 스레드는 더 이상 해당 잠금을 획득할 수 없습니다.
python/cpython#129536는 이 문제의 한 예입니다. 해당 이슈에서 데몬 스레드가 sys.stderr의 잠금을 보유한 채 멈췄기 때문에 주 스레드가 더 이상 이를 획득할 수 없었으며, 그 결과 Python은 종료 처리 중 치명적 오류를 발생시킵니다.
명세
Note
이 PEP는 현재 작성된 내용으로는 C API 작업 그룹( PEP 731 참조)의 승인을 받지 못했습니다. 그러나 이 제안의 작성자는 해당 그룹과 이 PEP에 대해 상당히 논의했으며, 이전 버전의 이 PEP는 실제로 승인을 받았습니다. Python 운영 위원회와 논의한 후, 이 PEP의 작성자는 Python 생태계에 광범위한 변경을 가하는 일도 피하면서 가능한 최선의 설계를 만들려면 작업 그룹의 권고 일부에서 벗어나는 것이 올바른 방향이라고 판단했습니다.
인터프리터 가드
-
type PyInterpreterGuard
- An opaque interpreter guard structure.
By holding an interpreter guard, the caller can ensure that the interpreter will not finalize until the guard is closed (through
PyInterpreterGuard_Close()).This is similar to a “readers-writers” lock; threads may concurrently guard an interpreter, and the interpreter will have to wait until all threads have closed their guards before it can enter finalization. After finalization has started, threads are forever unable to acquire guards for that interpreter.
-
PyInterpreterGuard *PyInterpreterGuard_FromCurrent(void)
- Create a finalization guard for the current interpreter.
On success, this function returns a guard for the current interpreter (as determined by the attached thread state); on failure, it returns
NULLwith an exception set. This function will fail only if the current interpreter has already started finalizing, or if the process is out of memory.The guard pointer returned by this function must be eventually closed with
PyInterpreterGuard_Close(); failing to do so will result in the Python process infinitely hanging.The caller must hold an attached thread state.
-
PyInterpreterGuard *PyInterpreterGuard_FromView(PyInterpreterView *view)
- Create a finalization guard for an interpreter through a view.
view must not be
NULL.On success, this function returns a guard to the interpreter represented by view. The view is still valid after calling this function. The guard must eventually be closed with
PyInterpreterGuard_Close().If the interpreter no longer exists, is already finalizing, or out of memory, then this function returns
NULLwithout setting an exception.The caller does not need to hold an attached thread state.
-
void PyInterpreterGuard_Close(PyInterpreterGuard *guard)
- Close an interpreter guard, allowing the interpreter to enter
finalization if no other guards remain. If an interpreter guard
is never closed, the interpreter will infinitely wait when trying
to enter finalization.
After an interpreter guard is closed, it may not be used in
PyThreadState_Ensure(). Doing so will result in undefined behavior.This function cannot fail, and the caller doesn’t need to hold an attached thread state.
인터프리터 뷰
-
type PyInterpreterView
- An opaque view of an interpreter.
This is a thread-safe way to access an interpreter that may have be finalizing or already destroyed.
-
PyInterpreterView *PyInterpreterView_FromCurrent(void)
- Create a view to the current interpreter.
This function is generally meant to be used alongside
PyInterpreterGuard_FromView()orPyThreadState_EnsureFromView().On success, this function returns a view to the current interpreter; on failure, it returns
NULLwith an exception set.The caller must hold an attached thread state.
-
void PyInterpreterView_Close(PyInterpreterView *view)
- Delete an interpreter view. If an interpreter view is never closed, the
view’s memory will never be freed, but there are no other consequences.
(In contrast, forgetting to close a guard will infinitely hang the main
thread during finalization.)
This function cannot fail, and the caller doesn’t need to hold an attached thread state.
-
PyInterpreterView *PyInterpreterView_FromMain()
- Create a view for the main interpreter (the first and default
interpreter in a Python process).
On success, this function returns a view to the main interpreter; on failure, it returns
NULLwithout an exception set. Failure indicates that the process is out of memory.The caller does not need to hold an attached thread state.
스레드 상태 연결 및 분리
이 제안에는 PyGILState_Ensure()와 PyGILState_Release()를 대체하려는 세 가지 새로운 고수준 스레딩 API가 포함되어 있습니다.
-
PyThreadStateToken *PyThreadState_Ensure(PyInterpreterGuard *guard)
- Ensure that the thread has an attached thread state for the
interpreter protected by guard, and thus can safely invoke that
interpreter.
It is OK to call this function if the thread already has an attached thread state, as long as there is a subsequent call to
PyThreadState_Release()that matches this one.Nested calls to this function will only sometimes create a new thread state.
First, this function checks if an attached thread state is present. If there is, this function then checks if the interpreter of that thread state matches the interpreter guarded by guard. If that is the case, this function simply marks the thread state as being used by a
PyThreadState_Ensurecall and returns.If there is no attached thread state, then this function checks if any thread state has been used by the current OS thread. (This is returned by
PyGILState_GetThisThreadState().) If there was, then this function checks if that thread state’s interpreter matches guard. If it does, it is re-attached and marked as used.Otherwise, if both of the above cases fail, a new thread state is created for guard. It is then attached and marked as owned by
PyThreadState_Ensure.This function will return
NULLto indicate a memory allocation failure, and otherwise return a token indicating the thread state that was previously attached (which might have beenNULL, in which case an non-NULLsentinel value is returned instead to differentiate between failure).
-
PyThreadStateToken *PyThreadState_EnsureFromView(PyInterpreterView *view)
- Get an attached thread state for the interpreter referenced by view.
view must not be
NULL. If the interpreter referenced by view has been finalized or is currently finalizing, then this function returnsNULLwithout setting an exception. This function may also returnNULLto indicate that the process is out of memory.The interpreter referenced by view will be implicitly guarded. The guard will be released upon the corresponding
PyThreadState_Release()call.On success, this function will return the thread state that was previously attached. If no thread state was previously attached, this returns a non-
NULLsentinel value. The behavior of whether this function creates a thread state is equivalent to that ofPyThreadState_Ensure().
-
void PyThreadState_Release(PyThreadStateToken *token)
- Release a
PyThreadState_Ensure()call. This must be called exactly once for each call toPyThreadState_Ensure. The attached thread state used prior to thePyThreadState_Ensurecall will be restored upon returning.token must be the return value from the most recent
PyThreadState_Ensurecall.This function will decrement an internal counter on the attached thread state. If this counter ever reaches below zero, this function emits a fatal error (via
Py_FatalError()).If the attached thread state is owned by
PyThreadState_Ensure, then the attached thread state will be deallocated and deleted upon the internal counter reaching zero. Otherwise, nothing happens when the counter reaches zero.
PyGILState API의 소프트 사용 중단
이 제안은 기존 및 새 PyThreadState API를 사용하는 대신 기존의 모든 PyGILState API에 소프트 사용 중단을 적용합니다. 소프트 사용 중단은 이러한 API를 더 이상 개발하지 않는다는 의미일 뿐이며, Python의 C API에서 PyGILState를 제거할 계획은 없습니다.
다음은 소프트 사용 중단된 함수와 그 대체 항목의 전체 목록입니다.
PyGILState_Ensure(): 대신PyThreadState_Ensure()를 사용하십시오.PyGILState_Release(): 대신PyThreadState_Release()를 사용하십시오.PyGILState_GetThisThreadState(): 대신PyThreadState_Get()또는PyThreadState_GetUnchecked()를 사용하십시오.PyGILState_Check(): 대신PyThreadState_GetUnchecked() != NULL을 사용하십시오.
제한된 API에 추가되는 항목
이 PEP의 모든 API를 제한된 C API에 추가합니다.
PyThreadState_Ensure()PyThreadState_EnsureFromView()PyThreadState_Release()PyInterpreterView(불투명 구조체)PyInterpreterView_FromCurrent()PyInterpreterView_Close()PyInterpreterView_FromMain()PyInterpreterGuard(불투명 구조체로서)PyInterpreterGuard_FromCurrent()PyInterpreterGuard_FromView()PyInterpreterGuard_Close()
근거
PyGILState를 수정하는 대신 새로운 API를 사용하는 이유는 무엇입니까?
PyGILState에서 “GIL”이라는 용어는 자유 스레딩에서 혼란을 일으킵니다.
이 PEP가 PyGILState대신 PyThreadState를 접두사로 사용하는 이유는 C API에서 “GIL”이라는 용어가 의미상 오해를 불러일으키기 때문입니다. 최신 Python 버전에서 PyGILState_Ensure()는 스레드 상태를 연결하는 기능에 관한 것이며, GIL을 획득하는 것은 부수적인 동작일 뿐입니다.
자유 스레드 빌드에서 C API를 호출하려면 연결된 스레드 상태가 여전히 필요하지만, 이름에 “GIL”이 포함되어 있으면 외부 스레드에서 PyGILState_Ensure와 PyGILState_Release를 여전히 호출해야 하는 이유가 혼란스러운 경우가 많습니다.
PyGILState_Ensure의 종료 처리 동작은 변경할 수 없습니다.
Python 프로그램에는 PyGILState_Ensure()가 더 이상 스레드 상태를 연결할 수 없는 시점이 항상 존재합니다. 인터프리터가 이미 완전히 종료되었다면 Python이 스레드에 해당 인터프리터를 호출할 방법을 제공할 수 없음은 명백합니다. 안타깝게도 PyGILState_Ensure()는 실패를 의미 있게 반환할 방법이 없으므로, 스레드를 종료하거나 중단된 상태로 두거나 치명적 오류를 발생시키는 수밖에 없습니다. 예를 들어, 이는 python/cpython#124622에서 논의되었습니다:
GIL을 획득하고 해제하는 새로운 C API가 필요하다고 생각합니다. 기존 C 코드에서 기존 API를 사용하는 방식은 오류 상태를 갑자기 덧붙이기에는 적합하지 않으며, 기존 C 코드 중 어느 것도 그런 방식으로 작성되지 않았습니다. 호출이 끝나면 기존 API는 항상 GIL을 보유하고 계속 진행할 수 있다고 가정합니다. 이 API는 다른 선택지 없이 “GIL을 획득할 때까지 차단하고 획득한 후에만 반환하도록” 설계되었습니다.
PyGILState_Ensure는 잘못된 (하위)인터프리터를 사용할 수 있습니다.
현재 PyGILState함수는 서브인터프리터에서 지원되지 않는 것으로 문서화되어 있습니다.
이는 PyGILState_Ensure()가 해당 스레드를 생성한 인터프리터가 어느 것인지 알 방법이 없으므로, 해당 인터프리터가 메인 인터프리터라고 가정해야 하기 때문입니다. 이로 인해 일부 불필요한 문제가 발생할 수 있습니다.
예를 들어 다음과 같습니다:
- 메인 스레드가 서브스레드를 생성하는 서브인터프리터에 진입합니다.
- 서브스레드는 자신을 생성한 인터프리터가 어느 것인지 알지 못한 채
PyGILState_Ensure()를 호출합니다. 따라서 서브스레드는 메인 인터프리터의 GIL을 획득합니다. - 이제 서브스레드는 서브인터프리터를 위한 일부 리소스를 실행하려고 할 수 있습니다. 예를 들어, 해당 스레드에
list객체에 대한PyObject *참조가 전달되었을 수 있습니다. - 서브스레드는
list.append()를 호출하며, 이 메서드는 리스트의 내부 버퍼 크기를 조정하기 위해PyMem_Realloc()을 호출하려고 합니다. PyMem_Realloc은 서브인터프리터의 할당자가 아니라 주 인터프리터의 할당자를 사용합니다. 연결된 스레드 상태(PyGILState_Ensure에서 가져옴)가 주 인터프리터를 가리키기 때문입니다.PyMem_Realloc은 리스트의 버퍼를 소유하지 않습니다. 충돌합니다!
이 PEP의 작성자는 현재 서브인터프리터가 널리 사용되는 사례는 아니라는 점을 인정하지만, 서브인터프리터에 대한 상황도 개선하지 않는 새 API를 설계하기는 어려울 것이라고 믿습니다. 서브인터프리터 지원을 선택적으로 제외하는 기능은 PyInterpreterView_FromMain()을 통해 사용할 수 있습니다.
하위 호환성
이 PEP는 호환성을 깨뜨리는 변경 사항을 지정하지 않습니다.
기존 코드는 이 PEP의 새 API를 사용하도록 다시 작성할 필요가 없습니다, 모든 PyGILState API는 계속 작동합니다. PyGILState API를 사용해도 컴파일 중이나 런타임에 어떠한 형태의 경고도 발생하지 않습니다. 향후 Python 버전에는 새로운 PyGILState API가 없을 뿐입니다.
보안 관련 사항
이 PEP에는 알려진 보안 관련 사항이 없습니다.
이 내용을 가르치는 방법
모든 C API 함수와 마찬가지로 이 PEP의 모든 새 API는 C API 문서에 문서화됩니다.
예제
예제: 라이브러리 인터페이스
로깅을 위한 C 라이브러리를 개발한다고 가정하십시오. 사용자가 Python 파일 객체에 로그를 기록할 수 있도록 하는 API를 제공하고 싶을 수 있습니다.
이 PEP를 사용하면 다음과 같이 구현할 수 있습니다.
/* Log to a Python file. No attached thread state is required by the caller. */
int
log_to_py_file_object(PyInterpreterView *view, PyObject *file,
PyObject *text)
{
assert(view != NULL);
PyThreadStateToken *token = PyThreadState_EnsureFromView(view);
if (tstate == NULL) {
fputs("Cannot call Python.\n", stderr);
return -1;
}
const char *to_write = PyUnicode_AsUTF8(text);
if (to_write == NULL) {
// Since the exception may be destroyed upon calling PyThreadState_Release(),
// print out the exception ourselves.
PyErr_Print();
PyThreadState_Release(token);
return -1;
}
int res = PyFile_WriteString(to_write, file);
if (res < 0) {
PyErr_Print();
}
PyThreadState_Release(token);
return res < 0;
}
예제: 잠금 보호
이 예제에서는 C에서 정의된 Python 메서드에서 C 잠금을 획득하는 방법을 보여 줍니다.
이 메서드가 데몬 스레드에서 호출되면 인터프리터가 스레드 상태를 다시 연결하는 동안 해당 스레드를 중단시킬 수 있으며, 그러면 잠금이 획득된 상태로 남게 됩니다. 이 경우 잠금을 획득하려는 이후의 모든 파이널라이저가 교착 상태에 빠집니다.
잠금이 유지되는 동안 인터프리터를 보호하면 스레드가 강제로 종료되거나 중단되지 않는다고 확신할 수 있습니다.
static PyObject *
critical_operation(PyObject *self, PyObject *Py_UNUSED(args))
{
assert(PyThreadState_GetUnchecked() != NULL);
PyInterpreterGuard *guard = PyInterpreterGuard_FromCurrent();
if (guard == NULL) {
/* Python is already finalizing or out of memory. */
return NULL;
}
Py_BEGIN_ALLOW_THREADS;
PyMutex_Lock(&global_lock);
/* Do something while holding the lock.
The interpreter won't finalize during this period. */
// ...
PyMutex_Unlock(&global_lock);
Py_END_ALLOW_THREADS;
PyInterpreterGuard_Close(guard);
Py_RETURN_NONE;
}
예제: PyGILState API에서 마이그레이션하기
다음 코드는 PyGILState API를 사용합니다.
static int
thread_func(void *arg)
{
PyGILState_STATE gstate = PyGILState_Ensure();
/* It's not an issue in this example, but we just attached
a thread state for the main interpreter. If my_method() was
originally called in a subinterpreter, then we would be unable
to safely interact with any objects from it. */
// This can hang the thread during finalization, because print() will
// detach the thread state while writing to stdout.
if (PyRun_SimpleString("print(42)") < 0) {
PyErr_Print();
}
PyGILState_Release(gstate);
return 0;
}
static PyObject *
my_method(PyObject *self, PyObject *unused)
{
PyThread_handle_t handle;
PyThead_indent_t indent;
if (PyThread_start_joinable_thread(thread_func, NULL, &ident, &handle) < 0) {
return NULL;
}
// Join the thread, for example's sake.
Py_BEGIN_ALLOW_THREADS;
PyThread_join_thread(handle);
Py_END_ALLOW_THREADS;
Py_RETURN_NONE;
}
다음은 새 함수를 사용하도록 다시 작성한 동일한 코드입니다.
static int
thread_func(void *arg)
{
PyInterpreterGuard *guard = (PyInterpreterGuard *)arg;
PyThreadStateToken *token = PyThreadState_Ensure(guard);
if (token == NULL) {
PyInterpreterGuard_Close(guard);
return -1;
}
if (PyRun_SimpleString("print(42)") < 0) {
PyErr_Print();
}
PyThreadState_Release(token);
PyInterpreterGuard_Close(guard);
return 0;
}
static PyObject *
my_method(PyObject *self, PyObject *unused)
{
PyThread_handle_t handle;
PyThead_indent_t indent;
PyInterpreterGuard *guard = PyInterpreterGuard_FromCurrent();
if (guard == NULL) {
return NULL;
}
if (PyThread_start_joinable_thread(thread_func, guard, &ident, &handle) < 0) {
PyInterpreterGuard_Close(guard);
return NULL;
}
Py_BEGIN_ALLOW_THREADS
PyThread_join_thread(handle);
Py_END_ALLOW_THREADS
Py_RETURN_NONE;
}
예제: 데몬 스레드
이 PEP에서 “데몬” 스레드(즉, 인터프리터 종료 중 스레드 상태를 연결할 때 멈추는 스레드)는 현재 C API에서 외부 스레드가 작동하는 방식과 매우 유사합니다. 다음 PyThreadState_Ensure()를 호출한 후에는 인터프리터 가드를 간단히 닫아 인터프리터가 종료되고 현재 스레드가 영원히 멈추도록 하십시오. 관련 인터프리터 가드가 스레드 상태에 의해 소유되므로 PyThreadState_EnsureFromView()를 사용할 때는 이것이 불가능하다는 점에 유의할 필요가 있습니다.
static int
thread_func(void *arg)
{
PyInterpreterGuard *guard = (PyInterpreterGuard *)arg;
PyThreadStateToken *token = PyThreadState_Ensure(guard);
if (token == NULL) {
PyInterpreterGuard_Close(guard);
return -1;
}
// If no other guards are left, the interpreter may now finalize.
PyInterpreterGuard_Close(guard);
// This will detach the thread state while writing to stdout, which
// will in turn allow for the thread to hang when attempting to reattach.
if (PyRun_SimpleString("print(42)") < 0) {
PyErr_Print();
}
PyThreadState_Release(token);
return 0;
}
static PyObject *
my_method(PyObject *self, PyObject *unused)
{
PyThread_handle_t handle;
PyThead_indent_t indent;
PyInterpreterGuard *guard = PyInterpreterGuard_FromCurrent();
if (guard == NULL) {
return NULL;
}
if (PyThread_start_joinable_thread(thread_func, guard, &ident, &handle) < 0) {
PyInterpreterGuard_Close(guard);
return NULL;
}
Py_RETURN_NONE;
}
예제: 비동기 콜백
static int
async_callback(void *arg)
{
PyInterpreterView *view = (PyInterpreterView *)arg;
// Try to create and attach a thread state based on our view.
PyThreadStateToken *token = PyThreadState_EnsureFromView(view);
if (token == NULL) {
PyInterpreterView_Close(view);
return -1;
}
// Execute our Python code, now that we have an attached thread state.
if (PyRun_SimpleString("print(42)") < 0) {
PyErr_Print();
}
PyThreadState_Release(token);
// In this example, we'll close the view for completeness.
// If we wanted to use this callback again, we'd have to keep it alive.
PyInterpreterView_Close(view);
return 0;
}
static PyObject *
setup_callback(PyObject *self, PyObject *unused)
{
PyInterpreterView *view = PyInterpreterView_FromCurrent();
if (view == NULL) {
return NULL;
}
MyNativeLibrary_RegisterAsyncCallback(async_callback, view);
Py_RETURN_NONE;
}
예제: 자체 PyGILState_Ensure 구현하기
다음 PyInterpreterView_FromMain()을 사용하면 PyGILState_Ensure/PyGILState_Release의 동작을 재현할 수 있습니다. 예를 들어 다음과 같습니다.
PyThreadStateToken *
MyGILState_Ensure(void)
{
PyInterpreterView *view = PyInterpreterView_FromMain();
if (view == NULL) {
// Out of memory.
PyThread_hang_thread();
}
PyThreadStateToken *token = PyThreadState_EnsureFromView(view);
PyInterpreterView_Close(view);
if (token == NULL) {
// Main interpreter not available
PyThread_hang_thread();
}
return token;
}
#define MyGILState_Release PyThreadState_Release
참조 구현
이 PEP의 참조 구현은 python/cpython#133110에서 확인할 수 있습니다.
거부된 아이디어
PyThreadState_Ensure의 반환 값에 PyThreadState * 사용하기
이 PEP의 이전 개정판에서는 PyThreadState_Ensure() 및 PyThreadState_EnsureFromView()가 일반적인 PyThreadState *를 반환했습니다. 이는 작성 시점의 구현과 일치했으며, 구현은 일반적으로 유효한 PyThreadState *를 반환하지만, 이것이 사용자에게 혼란을 줄 수 있다는 사실이 발견되었습니다.
- 실제로 반환되는 값은 (
PyThreadState_Release호출에 전달하는 표시자)인데도, 반환된 값을 새로 연결된 스레드 상태로 착각하기 쉽습니다. - 반환 값이
PyThreadState *를 받는 다른 API에서도 유용할 것처럼 보이지만, 실제로는PyThreadState_Release()에 전달할 토큰으로만 유용합니다(포인터가 유효하지 않을 수 있기 때문입니다).
따라서 이 PEP는 새 PyThreadStateToken 유형 뒤에 스레드 상태 정보를 감춥니다.
PyGILState의 강제 지원 중단
이 PEP는 이전에는 PyGILState 계열의 모든 API를 “강제로” 지원 중단하고, Python 3.20(5년 후) 또는 Python 3.25(10년 후)에 제거할 계획을 명시했습니다.
PyGILState_Ensure에 몇 가지 근본적인 결함이 있다는 점은 인정되지만 20년 넘게 사용되어 왔으며, 모든 것을 마이그레이션하는 것은 Python 생태계에 너무 큰 변경이 될 뿐이므로 결국 이 방안은 채택되지 않았습니다.
이 PEP가 최종화 문제를 해결하더라도, 현재 PyGILState_Ensure를 사용하는 기존 코드의 대다수는 정상적으로 작동하며, 새로운 API가 존재하는지 여부와 관계없이 계속 작동할 것입니다.
인터프리터 참조 횟수 계산
이 제안에는 인터프리터가 참조 횟수를 유지하고 종료하기 전에 해당 횟수가 0에 도달할 때까지 기다리도록 명시한 두 가지 개정안이 있었습니다.
이 아이디어의 첫 번째 개정안은 PyInterpreterState *포인터에 암시적 참조 횟수 계산을 추가하는 방식으로 이를 구현했습니다. PyInterpreterState_Hold라는 함수는 참조 횟수를 증가시켜 “강한 참조”로 만들고, PyInterpreterState_Release는 이를 감소시키도록 했습니다. 인터프리터의 ID(독립적인 int64_t)는 약한 참조의 한 형태로 사용되었으며, 이를 통해 인터프리터 상태를 조회하고 해당 참조 횟수를 원자적으로 증가시킬 수 있었습니다.
이러한 아이디어는 결국 지나치게 혼란스러워 보였기 때문에 거부되었습니다. PyInterpreterState *의 모든 사용이 암시적으로 빌린 참조이거나 강한 참조가 되므로, 개발자가 코드의 어느 부분에서 강한 참조를 필요로 하거나 사용하는지 이해하기 어려워집니다. 이 문제는 Python의 C API에서 PyObject *에 대해서도 이미 인식된 바 있습니다.
이러한 반발에 대응하여 이 PEP는 참조 횟수 계산을 모방하면서도 개발자가 더 쉽게 이해할 수 있도록 보다 명시적인 방식으로 동작하는 PyInterpreterRefAPI를 명시했습니다. PyInterpreterRef는 이 PEP의 PyInterpreterGuard와 유사했습니다. 마찬가지로 이전 개정안에는 PyInterpreterView와 유사한 PyInterpreterWeakRef도 포함되어 있었습니다.
결국 몇 가지 이유로 이 제안에서 참조 횟수 계산이라는 개념은 완전히 폐기되었습니다.
- API 설계가 지나치게 복잡해질 수 있다는 우려가 있었습니다. 참조 횟수 계산 설계가 CPython에서 전례가 없었던 HPy의 설계와 매우 유사해 보였기 때문입니다. 이 제안이 CPython에 HPy를 도입하기 위한 선례로 사용될 수 있다는 우려도 있었습니다.
- 기존의 참조 횟수 계산 API와 달리 인터프리터에 대한 강한 참조를 얻는 작업은 언제든 실패할 수 있으며, 인터프리터는 참조 횟수가 0에 도달해도 즉시 할당 해제되지 않았습니다.
- 인터프리터에 “진정한” 참조 횟수 계산을 추가하자는 논의가 이전에 있었습니다(참조 횟수가 0에 도달하면 할당 해제되는 방식). CPython에 이와 다르게 동작하는
PyInterpreterRef라는 API가 이미 존재했다면 이는 매우 혼란스러웠을 것입니다.
데몬이 아닌 스레드 상태
이 PEP의 이전 개정판에서는 인터프리터 가드가 인터프리터의 속성이 아니라 스레드 상태의 속성일 뿐이었습니다. 이는 PyThreadState_Ensure()가 인터프리터 가드를 유지하도록 했으며, PyThreadState_Release()를 호출할 때 해당 가드가 해제되었다는 의미입니다. 인터프리터에 대한 가드를 보유한 스레드 상태를 “non-daemon thread state”라고 불렀습니다.
기능적으로 이 제안은 PyThreadState_EnsureFromView()에서 여전히 이러한 동작을 수행하지만, 이것이 기본 동작은 아닙니다. 또한 threading 스레드와 비교할 때 “non-daemon”이라는 용어는 혼동을 일으켰습니다. non-daemon Thread 객체는 명시적으로 조인되는 반면, non-daemon 외부 스레드는 가드를 해제할 때까지 기다리기만 하기 때문입니다.
반환 값으로 PyStatus를 PyThreadState_Ensure에 사용하기
이 API의 이전 버전에서는 PyThreadState_Ensure()가 실패를 나타내기 위해 PyStatus를 반환했으며, 이는 오류 메시지를 제공한다는 이점이 있었습니다.
오류 메시지가 실제로 그만큼 유용할지는 분명하지 않고, 새 API를 사용하기 더 번거롭게 만들 수 있기 때문에 이는 거부되었습니다.
감사의 말
이 PEP는 Victor Stinner, Antoine Pitrou, David Woods, Sam Gross, Matt Page, Ronald Oussoren, Matt Wozniski, Eric Snow, Steve Dower, Petr Viktorin, Gregory P.를 비롯한 많은 사람들의 이전 작업, 피드백 및 논의를 바탕으로 합니다. Smith, Alyssa Coghlan 및 Python의 2026 Steering Council에 감사드립니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.