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에서 “종료 처리(finalization)”라는 용어는 Python 런타임 전체가 아니라 단일 인터프리터의 종료 처리를 가리킵니다.
또한 이 PEP에서 “외부 스레드(foreign thread)”라는 용어는 threading 모듈로 생성되지 않은 스레드를 가리킵니다. threading 모듈로 생성된 스레드는 이 제안에서 때때로 “Python 스레드”라고 부릅니다.
동기
인터프리터 종료 처리 중에 외부 스레드가 멈추는 문제
많은 대규모 라이브러리는 대상 인터프리터가 종료 처리 중이거나 이미 종료 처리되었을 수 있는 매우 비동기적인 상황에서 Python 코드를 호출해야 할 수 있으며, 인터프리터를 호출한 뒤에도 코드를 계속 실행하기를 원합니다. 이러한 요구는 사용자들이 제기한 바 있습니다. 예를 들어, 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
- 불투명한 인터프리터 가드 구조체입니다.
인터프리터 가드를 보유하면, 호출자는 가드가 (
PyInterpreterGuard_Close()를 통해) 닫힐 때까지 인터프리터가 종료되지 않도록 보장할 수 있습니다.이는 “readers-writers” 잠금과 비슷합니다. 스레드들은 동시에 인터프리터를 보호할 수 있으며, 인터프리터는 모든 스레드가 자신의 가드를 닫을 때까지 기다린 후에야 종료 처리에 들어갈 수 있습니다. 종료 처리가 시작된 후에는 스레드가 해당 인터프리터에 대한 가드를 영원히 획득할 수 없습니다.
-
PyInterpreterGuard *PyInterpreterGuard_FromCurrent(void)
- 현재 인터프리터에 대한 종료 처리 가드를 생성합니다.
성공하면 이 함수는 (attached thread state에 의해 결정되는) 현재 인터프리터에 대한 가드를 반환하고, 실패하면 예외를 설정한 채
NULL을 반환합니다. 이 함수는 현재 인터프리터가 이미 종료를 시작했거나 프로세스의 메모리가 부족한 경우에만 실패합니다.이 함수가 반환한 가드 포인터는 결국
PyInterpreterGuard_Close()로 닫아야 합니다. 그렇게 하지 않으면 Python 프로세스가 무한히 멈추게 됩니다.호출자는 어태치된 스레드 상태를 보유해야 합니다.
-
PyInterpreterGuard *PyInterpreterGuard_FromView(PyInterpreterView *view)
- 뷰를 통해 인터프리터에 대한 종료 처리 가드를 생성합니다. view는
NULL이어서는 안 됩니다.성공하면 이 함수는 view가 나타내는 인터프리터에 대한 가드를 반환합니다. 이 함수를 호출한 후에도 뷰는 여전히 유효합니다. 가드는 결국
PyInterpreterGuard_Close()로 닫아야 합니다.인터프리터가 더 이상 존재하지 않거나, 이미 종료 중이거나, 메모리가 부족하면 이 함수는 예외를 설정하지 않고
NULL을 반환합니다.호출자는 어태치된 스레드 상태를 보유할 필요가 없습니다.
-
void PyInterpreterGuard_Close(PyInterpreterGuard *guard)
- 인터프리터 가드를 닫아, 남아 있는 다른 가드가 없으면 인터프리터가 종료 처리에 들어갈 수 있도록 합니다. 인터프리터 가드가 끝내 닫히지 않으면, 인터프리터는 종료 처리에 들어가려 할 때 무한히 기다리게 됩니다.
인터프리터 가드가 닫힌 후에는
PyThreadState_Ensure()에서 사용할 수 없습니다. 그렇게 하면 정의되지 않은 동작이 발생합니다.이 함수는 실패할 수 없으며, 호출자는 어태치된 스레드 상태를 보유할 필요가 없습니다.
인터프리터 뷰
-
type PyInterpreterView
- 인터프리터에 대한 불투명한 뷰입니다.
이는 종료 중이거나 이미 소멸되었을 수 있는 인터프리터에 스레드 안전하게 접근하는 방법입니다.
-
PyInterpreterView *PyInterpreterView_FromCurrent(void)
- 현재 인터프리터에 대한 뷰를 생성합니다.
이 함수는 일반적으로
PyInterpreterGuard_FromView()또는PyThreadState_EnsureFromView()와 함께 사용하도록 의도되었습니다.성공하면 이 함수는 현재 인터프리터에 대한 뷰를 반환하고, 실패하면 예외를 설정한 채
NULL을 반환합니다.호출자는 어태치된 스레드 상태를 보유해야 합니다.
-
void PyInterpreterView_Close(PyInterpreterView *view)
- 인터프리터 뷰를 삭제합니다. 인터프리터 뷰가 끝내 닫히지 않으면 뷰의 메모리가 결코 해제되지 않지만, 그 외의 다른 결과는 없습니다. (이와 대조적으로, 가드를 닫는 것을 잊으면 종료 처리 중에 메인 스레드가 무한히 멈추게 됩니다.)
이 함수는 실패할 수 없으며, 호출자는 어태치된 스레드 상태를 보유할 필요가 없습니다.
-
PyInterpreterView *PyInterpreterView_FromMain()
- 메인 인터프리터(Python 프로세스의 첫 번째이자 기본 인터프리터)에 대한 뷰를 생성합니다.
성공하면 이 함수는 메인 인터프리터에 대한 뷰를 반환하고, 실패하면 예외를 설정하지 않고
NULL을 반환합니다. 실패는 프로세스의 메모리가 부족하다는 것을 의미합니다.호출자가 연결된 스레드 상태를 보유하고 있을 필요는 없습니다.
스레드 상태 연결 및 분리
이 제안에는 PyGILState_Ensure()와 PyGILState_Release()를 대체하기 위한 세 가지 새로운 고수준 스레딩 API가 포함됩니다.
-
PyThreadStateToken *PyThreadState_Ensure(PyInterpreterGuard *guard)
- 스레드가 guard로 보호되는 인터프리터에 대해 연결된 스레드 상태를 갖도록 하여, 해당 인터프리터를 안전하게 호출할 수 있게 합니다.
스레드에 이미 연결된 스레드 상태가 있더라도 이 함수를 호출해도 됩니다. 단, 이후에 이 호출과 짝을 이루는
PyThreadState_Release()호출이 있어야 합니다.이 함수를 중첩 호출한다고 해서 항상 새 스레드 상태가 생성되는 것은 아닙니다.
먼저, 이 함수는 연결된 스레드 상태가 있는지 확인합니다. 있다면, 이 함수는 그 스레드 상태의 인터프리터가 guard가 보호하는 인터프리터와 일치하는지 확인합니다. 일치한다면, 이 함수는 해당 스레드 상태를
PyThreadState_Ensure호출이 사용 중인 것으로 표시하고 반환하기만 합니다.연결된 스레드 상태가 없다면, 이 함수는 현재 OS 스레드가 사용한 적이 있는 스레드 상태가 있는지 확인합니다. (이 스레드 상태는
PyGILState_GetThisThreadState()가 반환합니다.) 있었다면, 이 함수는 그 스레드 상태의 인터프리터가 guard와 일치하는지 확인합니다. 일치한다면, 그 스레드 상태를 다시 연결하고 사용 중인 것으로 표시합니다.그렇지 않고 위의 두 경우가 모두 해당하지 않으면, guard에 대한 새 스레드 상태가 생성됩니다. 그런 다음 이 스레드 상태가 연결되고
PyThreadState_Ensure가 소유한 것으로 표시됩니다.이 함수는 메모리 할당 실패를 나타낼 때는
NULL을 반환하고, 그 외에는 이전에 연결되어 있던 스레드 상태를 나타내는 토큰을 반환합니다(그 스레드 상태가NULL이었을 수도 있으며, 이 경우에는 실패와 구별하기 위해NULL이 아닌 센티널 값이 대신 반환됩니다).
-
PyThreadStateToken *PyThreadState_EnsureFromView(PyInterpreterView *view)
- view가 참조하는 인터프리터에 대한 연결된 스레드 상태를 얻습니다.
view는
NULL이어서는 안 됩니다. view가 참조하는 인터프리터가 이미 파이널라이즈되었거나 현재 파이널라이즈 중이면, 이 함수는 예외를 설정하지 않고NULL을 반환합니다. 이 함수는 프로세스의 메모리가 부족함을 나타내기 위해NULL을 반환할 수도 있습니다.view가 참조하는 인터프리터는 암묵적으로 보호됩니다. 이 보호는 대응하는
PyThreadState_Release()호출 시 해제됩니다.성공하면, 이 함수는 이전에 연결되어 있던 스레드 상태를 반환합니다. 이전에 연결된 스레드 상태가 없었다면,
NULL이 아닌 센티널 값을 반환합니다. 이 함수가 스레드 상태를 생성하는지 여부는PyThreadState_Ensure()와 같은 방식으로 결정됩니다.
-
void PyThreadState_Release(PyThreadStateToken *token)
PyThreadState_Ensure()호출을 해제합니다. 이 함수는PyThreadState_Ensure를 호출할 때마다 정확히 한 번씩 호출해야 합니다. 이 함수가 반환되면,PyThreadState_Ensure호출 이전에 사용되던 연결된 스레드 상태가 복원됩니다.토큰 token은 가장 최근의
PyThreadState_Ensure호출에서 받은 반환값이어야 합니다.이 함수는 연결된 스레드 상태의 내부 카운터를 감소시킵니다. 이 카운터가 0 미만으로 내려가면, 이 함수는 (
Py_FatalError()를 통해) 치명적인 오류를 발생시킵니다.연결된 스레드 상태를
PyThreadState_Ensure가 소유하고 있다면, 내부 카운터가 0에 도달할 때 그 스레드 상태가 할당 해제되고 삭제됩니다. 그렇지 않으면, 카운터가 0에 도달해도 아무 일도 일어나지 않습니다.
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 코드 중 그런 식으로 작성된 것은 하나도 없습니다. 호출한 뒤에는 항상 GIL을 가지고 있으며 계속 진행할 수 있다고 가정할 뿐입니다. 이 API는 다른 선택지 없이 “GIL을 얻을 때까지 블록되며, 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에 몇 가지 근본적인 결함이 있다는 점은 인정하지만, 이 API는 20년 넘게 잘 동작해 왔고, 모든 것을 마이그레이션하는 것은 Python 생태계에 너무 큰 변화이기 때문입니다.
이 PEP가 다루는 종료 처리 문제가 있다 하더라도, PyGILState_Ensure를 사용하는 기존 코드의 대다수는 현재 잘 동작하며, 새로운 API의 존재 여부와 관계없이 계속 동작할 것입니다.
인터프리터 참조 카운팅
이 제안에는 두 가지 버전이 있었으며, 둘 다 인터프리터가 참조 횟수를 유지하고 종료하기 전에 그 횟수가 0이 될 때까지 기다리도록 명시했습니다.
이 아이디어의 첫 번째 반복안은 PyInterpreterState * 포인터에 암묵적인 참조 카운팅을 추가하는 방식으로 이를 구현했습니다. PyInterpreterState_Hold라는 함수는 참조 횟수를 증가시키고(이를 “강한 참조”로 만듦), PyInterpreterState_Release는 참조 횟수를 감소시켰습니다. 인터프리터의 ID(독립적인 int64_t)는 일종의 약한 참조로 사용되었으며, 이를 통해 인터프리터 상태를 조회하고 그 참조 횟수를 원자적으로 증가시킬 수 있었습니다.
이 아이디어들은 상황을 매우 혼란스럽게 만드는 것으로 보였기 때문에 결국 거부되었습니다. PyInterpreterState *의 모든 사용이 암묵적으로 빌린 참조이거나 강한 참조가 되므로, 개발자가 코드의 어느 부분이 강한 참조를 요구하거나 사용하는지 파악하기 어려워집니다. 이 문제는 Python C API의 PyObject *에서 이미 인정된 바 있습니다.
이러한 반발에 대응하여, 이 PEP는 마찬가지로 참조 카운팅을 흉내 내지만 더 명시적인 방식으로 개발자가 다루기 쉽게 만든 PyInterpreterRef API를 명시했습니다. PyInterpreterRef는 이 PEP의 PyInterpreterGuard에 해당했습니다. 마찬가지로, 이전 개정판에는 PyInterpreterWeakRef가 포함되어 있었으며, 이는 PyInterpreterView에 해당했습니다.
결국 몇 가지 이유로 참조 카운팅 개념은 이 제안에서 완전히 폐기되었습니다.
- API 설계가 지나치게 복잡하다는 우려가 있었습니다. 참조 카운팅 설계가 CPython에 선례가 없는 HPy의 설계와 매우 유사해 보였습니다. 이 제안이 HPy를 CPython에 도입하는 선례로 사용될 수 있다는 우려가 있었습니다.
- 전통적인 참조 카운팅 API와 달리, 인터프리터에 대한 강한 참조를 획득하는 작업은 언제든지 실패할 수 있었으며, 인터프리터는 참조 횟수가 0에 도달해도 즉시 할당 해제되지 않았습니다.
- 인터프리터에 (0에 도달하면 할당 해제되는) “진정한” 참조 카운팅을 추가하는 것에 대한 논의가 이전에 있었는데, CPython에 이미
PyInterpreterRef라는 이름으로 다른 일을 하는 API가 존재했다면 매우 혼란스러웠을 것입니다.
비데몬 스레드 상태
이 PEP의 이전 개정판에서 인터프리터 가드는 인터프리터의 속성이 아니라 오직 스레드 상태의 속성이었습니다. 이는 PyThreadState_Ensure()가 인터프리터 가드를 계속 보유하고 있다가, PyThreadState_Release()를 호출하면 그 가드가 닫힌다는 뜻이었습니다. 인터프리터에 대한 가드를 가진 스레드 상태는 “비데몬 스레드 상태(non-daemon thread state)”라고 불렸습니다.
기능적으로 이 제안은 PyThreadState_EnsureFromView()에서 여전히 이 동작을 제공하지만, 이것이 기본 동작은 아닙니다. 또한 “비데몬”이라는 용어는 threading 스레드와 대비되어 혼란을 주었습니다. 비데몬 Thread 객체는 명시적으로 조인되는 반면, 비데몬 외부 스레드는 가드를 해제하기를 기다리기만 하기 때문입니다.
PyThreadState_Ensure의 반환값으로 PyStatus사용하기
이 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 운영 위원회를 비롯한 많은 분의 선행 작업, 피드백, 논의를 바탕으로 합니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.