Free Threading을 위한 C API 확장 지원¶
3.13 버전부터 CPython은 free threading 이라는 설정에서 global interpreter lock (GIL)을 비활성화한 상태로 실행하는 기능을 지원합니다. 이 문서는 C API 확장을 자유 스레딩(free threading)이 지원하도록 조정하는 방법을 설명합니다.
C에서 Free-Threaded 빌드 식별하기¶
CPython C API는 Py_GIL_DISABLED 매크로를 노출합니다: free-threaded 빌드에서는 이 매크로가 1 로 정의되고, 일반 빌드에서는 정의되지 않습니다. 이를 사용하여 free-threaded 빌드에서만 실행되는 코드를 활성화할 수 있습니다:
들여쓰기 포함된 원문 유지:
#ifdef Py_GIL_DISABLED
/* code that only runs in the free-threaded build */
#endif
참고
Windows에서 이 매크로는 자동으로 정의되지 않으며, 빌드 시 컴파일러에 명시해야 합니다. 현재 실행 중인 인터프리터가 해당 매크로를 포함하고 있는지 여부는 sysconfig.get_config_var() 함수를 사용하여 확인할 수 있습니다.
모듈 초기화¶
확장 모듈은 GIL이 비활성화된 상태에서의 실행을 지원한다는 것을 명시적으로 나타내야 합니다. 그렇지 않으면 확장 모듈을 임포트할 때 경고가 발생하고 런타임에 GIL이 활성화됩니다.
확장 모듈이 다단계(multi-phase) 초기화 또는 단일 단계(single-phase) 초기화 중 어느 것을 사용하는지에 따라, GIL 비활성 실행 지원을 나타내는 두 가지 방법이 있습니다.
다단계 초기화¶
Extensions that use multi-phase initialization
(functions like PyModuleDef_Init(),
PyModExport_* export hook,
PyModule_FromSlotsAndSpec()) should add a
Py_mod_gil slot in the module definition.
If your extension supports older versions of CPython,
you should guard the slot with a PY_VERSION_HEX check.
static struct PyModuleDef_Slot module_slots[] = {
...
#if PY_VERSION_HEX >= 0x030D0000
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
단일 단계 초기화¶
레거시 단일 단계 초기화 (즉, PyModule_Create())를 사용하는 확장 모듈은 GIL 비활성 실행을 지원함을 나타내기 위해 PyUnstable_Module_SetGIL() 을 호출해야 합니다. 이 함수는 free-threaded 빌드에서만 정의되므로, 일반 빌드에서 컴파일 오류가 발생하지 않도록 #ifdef Py_GIL_DISABLED 로 감싸야 합니다.
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
...
};
PyMODINIT_FUNC
PyInit_mymodule(void)
{
PyObject *m = PyModule_Create(&moduledef);
if (m == NULL) {
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
}
일반 API 가이드라인¶
대부분의 C API는 스레드 안전(thread-safe)하지만, 몇 가지 예외가 있습니다.
구조체 필드: Python C API 객체 또는 구조체의 필드를 직접 액세스하는 것은 해당 필드가 동시에 수정될 수 있는 경우 스레드 안전하지 않습니다.
매크로:
PyList_GET_ITEM,PyList_SET_ITEM과 같은 접근 매크로나PySequence_Fast()가 반환한 객체를 사용하는PySequence_Fast_GET_SIZE와 같은 매크로는 에러 체크나 락킹을 수행하지 않습니다. 이러한 매크로는 컨테이너 객체가 동시에 수정될 수 있는 경우 스레드 안전하지 않습니다.빌린 참조: 빌린 참조 를 반환하는 C API 함수는 포함된 객체가 동시에 수정되는 경우 스레드 안전하지 않을 수 있습니다. 자세한 내용은 빌린 참조 섹션을 참조하십시오.
컨테이너의 스레드 안전성¶
PyListObject, PyDictObject, 그리고 PySetObject 와 같은 컨테이너는 free-threaded 빌드에서 내부적으로 락을 수행합니다. 예를 들어, PyList_Append() 는 항목을 추가하기 전에 리스트에 락을 겁니다.
PyDict_Next¶
주목할 만한 예외는 딕셔너리에 락을 걸지 않는 PyDict_Next() 입니다. 딕셔너리가 동시에 수정될 가능성이 있는 경우, 이를 반복(iterate)하는 동안 딕셔너리를 보호하기 위해 Py_BEGIN_CRITICAL_SECTION 을 사용해야 합니다.
Py_BEGIN_CRITICAL_SECTION(dict);
PyObject *key, *value;
Py_ssize_t pos = 0;
while (PyDict_Next(dict, &pos, &key, &value)) {
...
}
Py_END_CRITICAL_SECTION();
빌린 참조 (Borrowed References)¶
일부 C API 함수는 빌린 참조 를 반환합니다. 이러한 API들은 포함된 객체가 동시에 수정될 경우 스레드 안전(thread-safe)하지 않습니다. 예를 들어, 리스트가 동시에 수정될 가능성이 있는 상황에서 PyList_GetItem() 을 사용하는 것은 안전하지 않습니다.
다음 표는 몇몇 빌린 참조 API와, 대신 사용할 수 있는 강한 참조 를 반환하는 대체 API들을 나열합니다.
빌린 참조 API |
강한 참조 API |
|---|---|
없음 (참고: PyDict_Next) |
|
|
|
|
|
빌린 참조를 반환하는 모든 API가 문제가 되는 것은 아닙니다. 예를 들어, 튜플은 불변(immutable)이므로 PyTuple_GetItem() 은 안전합니다. 마찬가지로 위 API들의 모든 사용 사례가 문제가 되는 것도 아닙니다. 예를 들어, PyDict_GetItem() 은 함수 호출 시 키워드 인자 딕셔너리를 파싱하는 데 자주 사용되는데, 이러한 키워드 인자 딕셔너리는 사실상 비공개(다른 스레드에서 접근 불가)이므로 해당 맥락에서 빌린 참조를 사용하는 것은 안전합니다.
이 함수들 중 일부는 Python 3.13에서 추가되었습니다. 이전 버전의 Python에서도 이 기능들을 사용할 수 있도록 pythoncapi-compat 패키지를 사용할 수 있습니다.
메모리 할당 API¶
Python의 메모리 관리 C API는 “raw”, “mem”, “object”라는 세 가지 서로 다른 할당 영역 에서 함수를 제공합니다. 스레드 안전성을 위해 free-threaded 빌드에서는 오직 Python 객체만 object 영역을 사용하여 할당해야 하며, 모든 Python 객체는 반드시 해당 영역을 통해 할당되어야 합니다. 이는 이 규칙이 권장 사항일 뿐 필수 요구사항은 아니었던 이전 Python 버전들과는 다른 점입니다.
참고
확장 프로그램 내에서 PyObject_Malloc() 의 사용 사례를 찾아 할당된 메모리가 Python 객체용으로만 사용되는지 확인하십시오. 버퍼를 할당할 때는 PyObject_Malloc() 대신 PyMem_Malloc() 을 사용하십시오.
스레드 상태 및 GIL API¶
Python은 스레드 상태와 GIL을 관리하기 위한 일련의 함수와 매크로를 제공합니다:
이 함수들은 free-threaded 빌드에서 GIL 이 비활성화된 경우에도 스레드 상태를 관리하기 위해 여전히 사용되어야 합니다. 예를 들어, Python 외부에서 스레드를 생성하는 경우, 해당 스레드가 유효한 Python 스레드 상태를 갖도록 보장하기 위해 Python API를 호출하기 전에 PyThreadState_Ensure() 를 호출해야 합니다.
다른 스레드가 순환 가비지 컬렉터 를 실행할 수 있도록, I/O나 락 획득과 같은 차단(blocking) 작업 전후로 계속해서 PyEval_SaveThread() 또는 Py_BEGIN_ALLOW_THREADS 를 호출해야 합니다.
내부 확장 상태 보호¶
사용자의 확장 프로그램에는 이전에는 GIL에 의해 보호되던 내부 상태가 있을 수 있습니다. 이 상태를 보호하기 위해 락을 추가해야 할 수도 있습니다. 접근 방식은 확장 기능에 따라 다르지만, 몇 가지 일반적인 패턴은 다음과 같습니다.
캐시(Caches): 전역 캐시는 공유 상태의 흔한 원인입니다. 성능에 결정적이지 않다면 free-threaded 빌드에서 캐시 기능을 비활성화하거나, 락을 사용하여 캐시를 보호하는 것을 고려하십시오.
전역 상태(Global State): 전역 상태는 락에 의해 보호되거나 스레드 로컬 저장소로 이동해야 할 수 있습니다. C11 및 C++11은 스레드-로컬 저장소 를 위해
thread_local또는_Thread_local을 제공합니다.
크리티컬 섹션 (Critical Sections)¶
free-threaded 빌드에서 CPython은 기존에 GIL이 보호하던 데이터를 보호하기 위해 “크리티컬 섹션”이라는 메커니즘을 제공합니다. 확장 프로그램 개발자가 내부 크리티컬 섹션 구현과 직접 상호작용하지는 않더라도, free-threaded 빌드에서 특정 C API 함수를 사용하거나 공유 상태를 관리할 때 이들의 동작을 이해하는 것은 매우 중요합니다.
크리티컬 섹션이란 무엇인가?¶
개념적으로 크리티컬 섹션은 단순 뮤텍스 위에 구축된 데드락 방지 계층 역할을 합니다. 각 스레드는 활성화된 크리티컬 섹션의 스택을 유지합니다. 스레드가 특정 크리티컬 섹션과 관련된 락을 획득해야 할 때(예: PyDict_SetItem() 과 같은 스레드 안전 C API 함수를 호출할 때 암시적으로 또는 매크로를 사용하여 명시적으로), 해당 기초 뮤텍스를 획득하려고 시도합니다.
크리티컬 섹션 사용하기¶
크리티컬 섹션을 사용하는 주요 API는 다음과 같습니다.
Py_BEGIN_CRITICAL_SECTION및Py_END_CRITICAL_SECTION- 단일 객체 락을 위한 용도Py_BEGIN_CRITICAL_SECTION2및Py_END_CRITICAL_SECTION2- 두 객체를 동시에 락을 걸기 위한 용도
이 매크로들은 새로운 로컬 범위를 생성하므로 반드시 짝을 이루어 동일한 C 범위 내에 위치해야 합니다. 이 매크로들은 비(non)-free-threaded 빌드에서는 no-op으로 동작하므로, 두 빌드 유형을 모두 지원해야 하는 코드에 안전하게 추가할 수 있습니다.
크리티컬 섹션의 일반적인 사용 사례는 내부 속성에 접근하는 동안 객체에 락을 거는 것입니다. 예를 들어, 확장 타입이 내부 카운트 필드를 가지고 있는 경우, 해당 필드를 읽거나 쓸 때 크리티컬 섹션을 사용할 수 있습니다.
// 카운트 읽기, 내부 카운트 값에 대한 새로운 참조 반환
PyObject *result;
Py_BEGIN_CRITICAL_SECTION(obj);
result = Py_NewRef(obj->count);
Py_END_CRITICAL_SECTION();
return result;
// 카운트 쓰기, new_count로부터의 참조 소모
Py_BEGIN_CRITICAL_SECTION(obj);
obj->count = new_count;
Py_END_CRITICAL_SECTION();
크리티컬 섹션의 작동 원리¶
전통적인 락과 달리, 크리티컬 섹션은 전체 기간 동안 독점적 접근을 보장하지 않습니다. 만약 스레드가 크리티컬 섹션을 보유한 채로 차단(예: 다른 락 획득 또는 I/O 수행)될 경우, 해당 크리티컬 섹션은 일시적으로 중단되며(모든 락이 해제됨), 차단 작업이 완료되면 다시 재개됩니다.
이 동작은 스레드가 차단 호출을 수행할 때 GIL에서 발생하는 상황과 유사합니다. 주요 차이점은 다음과 같습니다.
크리티컬 섹션은 전역이 아닌 객체별로 작동함
크리티컬 섹션은 각 스레드 내에서 스택 규율을 따릅니다(“begin”과 “end” 매크로가 쌍으로 묶여 동일한 범위 내에 있어야 하므로 이 규칙이 강제됩니다)
크리티컬 섹션은 잠재적인 차단 작업 주변에서 락을 자동으로 해제하고 다시 획득함
데드락 방지¶
크리티컬 섹션은 두 가지 방식으로 데드락을 방지하는 데 도움을 줍니다.
스레드가 다른 스레드에 의해 이미 점유된 락을 획득하려고 하면, 먼저 활성화된 모든 크리티컬 섹션을 중단하고 관련 락들을 일시적으로 해제합니다.
차단 작업이 완료되면 가장 위에 있는(top-most) 크리티션 섹션부터 다시 획득합니다.
이는 내부 크리티컬 섹션이 외부의 것을 중단시킬 수 있으므로, 중첩된 크리티컬 섹션을 사용하여 여러 객체를 한꺼번에 락을 걸 수 없음을 의미합니다. 대신 두 개의 객체를 동시에 잠그려면 Py_BEGIN_CRITICAL_SECTION2 를 사용하십시오.
위에서 설명한 락은 오직 PyMutex 기반의 락입니다. 크리티컬 섹션 구현은 POSIX 뮤텍스와 같이 사용 중일 수 있는 다른 락 메커니즘에 대해 알지 못하며 영향을 주지도 않습니다. 또한, 어떤 PyMutex 에서든 블로킹이 발생하면 크리티컬 섹션이 중단되지만, 이 경우 크리티컬 섹션의 일부인 뮤텍스만 해제됩니다. 만약 크리티컬 섹션 없이 PyMutex 를 사용하는 경우에는 락이 해제되지 않으므로 동일한 데드록 방지 효과를 얻을 수 없습니다.
중요 고려 사항¶
크리티컬 섹션은 락을 일시적으로 해제하여 다른 스레드가 보호된 데이터를 수정할 수 있게 할 수 있습니다. 블록될 가능성이 있는 작업 이후의 데이터 상태에 대해 성급하게 가정하지 않도록 주의하십시오.
락이 일시적으로 해제(중단)될 수 있기 때문에, 크리티컬 섹션에 진입한다고 해서 해당 영역 내내 보호된 자원에 대한 독점적인 접근이 보장되는 것은 아닙니다. 크리티컬 섹션 내부의 코드가 차단되는 다른 함수를 호출하는 경우(예: 다른 락 획득, 블로킹 I/O 수행), 크리티컬 섹션을 통해 유지되던 모든 락이 해제됩니다. 이는 블록 호출 중에 GIL이 해제될 수 있는 방식과 유사합니다.
특정 시점에 유지됨이 보장되는 것은 가장 최근에 진입한(가장 위에 있는) 크리티컬 섹션과 관련된 락뿐입니다. 외부의 중첩된 크리티컬 섹션에 대한 락은 중단되었을 수 있습니다.
이 API를 사용하여 동시에 최대 두 개의 객체만 잠글 수 있습니다. 더 많은 객체를 잠그려면 코드 구조를 변경해야 합니다.
동일한 객체를 두 번 잠그려고 시도할 때 크리티컬 섹션이 데드락을 발생시키지는 않지만, 이 경우에 목적에 맞게 설계된 재진입 가능(reentrant) 록보다 효율성이 떨어집니다.
Py_BEGIN_CRITICAL_SECTION2를 사용할 때 객체의 순서는 정확성에 영향을 미치지 않지만(구현에서 데드록 방지를 처리함), 항상 일관된 순서로 객체를 잠그는 것이 좋습니다.크리티컬 섹션 매크로는 주로 위에서 설명한 데드록 시나리오에 노출될 수 있는 내부 CPython 작업에 관여하는 Python 객체 에 대한 접근을 보호하기 위한 것입니다. 순수하게 내부적인 확장 상태를 보호하려는 경우에는 표준 뮤텍스나 다른 동기화 프리미티브가 더 적절할 수 있습니다.
객체별 록 (ob_mutex)¶
자유 스레딩 빌드에서는 각 Python 객체가 ob_mutex 필드를 가지고 있으며, 이는 PyMutex 타입입니다. 이 뮤텍스는 중요 섹션 API (Py_BEGIN_CRITICAL_SECTION / Py_END_CRITICAL_SECTION)에 사용되도록 예약되어 있습니다.
경고
PyMutex_Lock(&obj->ob_mutex)``를 사용하여 ``ob_mutex``를 직접 잠그지 **마십시오**. 동일한 뮤텍스에 대해 직접적인 ``PyMutex_Lock 호출과 크리티컬 섹션 API를 혼용하면 데드록이 발생할 수 있습니다.
사용자의 코드에서 특정 객체 유형에 대해 크리티컬 섹션을 절대 사용하지 않더라도, CPython 내부 구성 요소가 모든 Python 객체에 대해 크리티컬 섹션 API를 사용할 수 있습니다.
확장 타입에 전용 록이 필요한 경우 객체 구조체에 별도의 PyMutex 필드(또는 다른 동기화 프리미티브)를 추가하십시오. PyMutex 는 매우 가벼우므로 하나를 더 추가하는 데 따르는 비용은 거의 없습니다.
free-threaded 빌드를 위한 확장 구축¶
C API 확장은 free-threaded 빌드를 위해 특별히 구축되어야 합니다. 휠(wheel), 공유 라이브러리 및 바이너리는 t 접미사로 표시됩니다.
pypa/manylinux <https://github.com/pypa/manylinux>`_는 ``python3.14t``와 같이 ``t` 접미사가 붙은 형태의 free-threaded 빌드를 지원합니다.
pypa/cibuildwheel 는 Python 3.14 및 그 이후 버전의 free-threaded 빌드용 휠 구축을 지원합니다.
제한적 C API 및 안정적 ABI¶
free-threaded 빌드는 현재 제한적 C API 또는 안정적 ABI를 지원하지 않습니다. setuptools 를 사용하여 확장 프로그램을 구축할 때 현재 py_limited_api=True 로 설정했다면, free-threaded 빌드 시에는 py_limited_api=not sysconfig.get_config_var("Py_GIL_DISABLED") 를 사용하여 제한적 API 옵션을 해제할 수 있습니다.
참고
free-threaded 빌드를 위해 별도의 휠을 구축해야 합니다. 현재 안정적 ABI를 사용하는 경우, 여러 비(non)-free-threaded Python 버전에 대해 하나의 휠을 계속해서 빌드할 수 있습니다.
Windows¶
공식 Windows 설치 프로그램의 제한으로 인해 소스에서 확장 프로그램을 빌드할 때 Py_GIL_DISABLED=1 을 수동으로 정의해야 합니다.
더 보기
Porting Extension Modules to Support Free-Threading <https://py-free-threading.github.io/porting/>: 확장 프로그램 작성자를 위한 커뮤니티 관리 포팅 가이드입니다.