PEP 539 – 새 CPython 스레드 로컬 저장소 C API
- Author:
- Erik M. Bray, Masayuki Yamamoto
- BDFL-Delegate:
- Alyssa Coghlan
- Status:
- Final
- Type:
- Standards Track
- Created:
- 20-Dec-2016
- Python-Version:
- 3.7
- Post-History:
- 16-Dec-2016, 31-Aug-2017, 08-Sep-2017
- Resolution:
- Python-Dev message
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
CPython에 새로운 스레드 로컬 저장소(Thread Local Storage, TLS) API를 추가하여 CPython 인터프리터 내부에서 기존 TLS API의 사용을 대체하는 동시에 기존 API를 더 이상 사용하지 않도록 하는 것이 제안의 내용입니다. 새로운 API의 이름은 “스레드 특정 저장소(Thread Specific Storage, TSS) API”입니다(이 이름의 유래는 Rationale for Proposed Solution의 설명을 참조하십시오).
기존 TLS API는 내부적으로만 사용되므로(문서에 언급되어 있지 않고, 이를 정의하는 헤더인 pythread.h도 Python.h에 직접 또는 간접적으로 포함되어 있지 않으므로), 이 제안은 아마도 CPython에만 영향을 주겠지만 CPython API의 일부를 구현하는 다른 인터프리터 구현(PyPy?)에도 영향을 줄 수 있습니다.
이는 주로 기존 API가 모든 플랫폼에서 TLS 키를 나타내는 데 int를 사용한다는 사실에 기인하며, 이는 POSIX를 준수하지도 않고 실제적인 의미에서 이식성이 있지도 않기 때문입니다 [1].
Note
이 문서 전체에서 “TLS”라는 약어는 스레드 로컬 저장소를 가리키며 “Transportation Layer Security” 프로토콜과 혼동해서는 안 됩니다.
사양
CPython 인터프리터 내부에서 사용되는 현재 TLS API는 6개의 함수로 구성됩니다.:
PyAPI_FUNC(int) PyThread_create_key(void)
PyAPI_FUNC(void) PyThread_delete_key(int key)
PyAPI_FUNC(int) PyThread_set_key_value(int key, void *value)
PyAPI_FUNC(void *) PyThread_get_key_value(int key)
PyAPI_FUNC(void) PyThread_delete_key_value(int key)
PyAPI_FUNC(void) PyThread_ReInitTLS(void)
이는 유사한 새로운 함수 집합으로 대체됩니다.:
PyAPI_FUNC(int) PyThread_tss_create(Py_tss_t *key)
PyAPI_FUNC(void) PyThread_tss_delete(Py_tss_t *key)
PyAPI_FUNC(int) PyThread_tss_set(Py_tss_t *key, void *value)
PyAPI_FUNC(void *) PyThread_tss_get(Py_tss_t *key)
이 사양은 몇 가지 새로운 기능도 추가합니다.
- 새로운
Py_tss_t타입—기반 TLS 구현에 따라 정의가 달라질 수 있는 불투명 타입입니다. 다음과 같이 정의됩니다.:typedef struct { int _is_initialized; NATIVE_TSS_KEY_T _key; } Py_tss_t;
여기서
NATIVE_TSS_KEY_T는 기반 네이티브 TLS 구현에 따라 값이 달라지는 매크로입니다(예:pthread_key_t). Py_tss_t변수의 초기화자인Py_tss_NEEDS_INIT입니다.- 새로운 세 개의 함수입니다.:
PyAPI_FUNC(Py_tss_t *) PyThread_tss_alloc(void) PyAPI_FUNC(void) PyThread_tss_free(Py_tss_t *key) PyAPI_FUNC(int) PyThread_tss_is_created(Py_tss_t *key)
처음 두 함수는
Py_tss_t의 동적 할당 및 할당 해제에 필요하며, 특히Py_LIMITED_API로 빌드된 확장 모듈에서 필요합니다. 이러한 모듈에서는 빌드 시점에 해당 타입의 구현이 불투명하므로 정적으로 할당할 수 없습니다.PyThread_tss_alloc이 반환하는 값은Py_tss_NEEDS_INIT으로 초기화된 값과 동일한 상태이거나, 동적 할당에 실패한 경우NULL입니다.PyThread_tss_free의 동작에는 예방적으로PyThread_tss_delete를 호출하는 것이 포함되며,key인자가 가리키는 값이NULL이면 아무 작업도 수행하지 않습니다.PyThread_tss_is_created는 주어진Py_tss_t가 초기화되어 있으면(즉,PyThread_tss_create에 의해 초기화되어 있으면) 0이 아닌 값을 반환합니다.
새로운 TSS API는 PyThread_delete_key_value 및 PyThread_ReInitTLS에 대응하는 함수를 제공하지 않습니다. 이러한 함수는 현재는 폐기된 CPython의 내장 TLS 구현에만 필요했기 때문입니다. 즉, 이러한 함수의 기존 동작은 다음과 같이 처리됩니다. PyThread_delete_key_value(key)는 PyThread_set_key_value(key, NULL)과 동등하며, PyThread_ReInitTLS()는 아무 작업도 수행하지 않습니다 [8].
새로운 PyThread_tss_ 함수는 몇 가지 사소한 차이를 제외하면 기존 대응 함수와 거의 정확히 유사합니다. 반면 PyThread_create_key는 인자를 받지 않고 TLS 키를 int로 반환하지만, PyThread_tss_create는 Py_tss_t*를 인자로 받고 int 상태 코드를 반환합니다. key 인자가 가리키는 값이 Py_tss_NEEDS_INIT으로 초기화되지 않은 경우 PyThread_tss_create의 동작은 정의되지 않습니다. 반환되는 상태 코드는 성공 시 0이고 실패 시 0이 아닌 값입니다. 0이 아닌 상태 코드의 의미는 이 사양에서 달리 정의하지 않습니다.
마찬가지로 다른 PyThread_tss_ 함수에도 Py_tss_t*가 전달되지만, 이전에는 키가 값으로 전달되었습니다. Py_tss_t 타입은 불투명 타입이므로 이론적으로 거의 어떤 크기든 될 수 있기 때문에 이러한 변경이 필요합니다. 타입의 크기를 알 수 없는 Py_LIMITED_API로 빌드된 확장 모듈에서는 이러한 변경이 특히 필요합니다. PyThread_tss_free를 제외하면, key 인자가 가리키는 값이 NULL인 경우 PyThread_tss_의 동작은 정의되지 않습니다.
또한 int 대신 Py_tss_t를 사용하기 때문에 새 API에는 키 생성 및 삭제와 관련하여 기존 API와 다른 동작이 있습니다. PyThread_tss_create는 동일한 키에 대해 반복해서 호출할 수 있으며, 이미 초기화된 키에 대해 호출하면 아무 작업도 하지 않고 즉시 성공을 반환합니다. 초기화되지 않은 키에 PyThread_tss_delete를 호출하는 경우도 마찬가지입니다.
PyThread_tss_delete의 동작은 키의 초기화 상태를 “초기화되지 않음”으로 변경하는 것으로 정의됩니다. 이를 통해 예를 들어 프로세스를 종료하지 않고 CPython 인터프리터를 다시 시작할 때 정적으로 할당된 키를 타당한 상태로 재설정할 수 있습니다(예: 애플리케이션에 Python을 임베드하는 경우) [12].
기존 PyThread_*_key*함수는 문서에서 더 이상 사용하지 않는 것으로 표시되지만, 런타임 사용 중단 경고를 생성하지는 않습니다.
또한 sizeof(pthread_key_t) != sizeof(int)인 플랫폼에서는 PyThread_create_key가 실패 상태를 반환하고 즉시 종료하며, 해당 플랫폼에서는 다른 TLS 함수가 모두 아무 작업도 수행하지 않습니다.
API 사양 비교
| API | Thread Local Storage (TLS) | Thread Specific Storage (TSS) |
|---|---|---|
| Version | Existing | New |
| Key Type | int |
Py_tss_t (opaque type) |
| Handle Native Key | cast to int |
conceal into internal field |
| Function Argument | int |
Py_tss_t * |
| Features |
|
|
| Key Initializer | (-1 as key creation
failure) |
Py_tss_NEEDS_INIT |
| Requirement | native threads (since CPython 3.7 [9]) | native threads |
| Restriction | No support for platforms
where native TLS key is
defined in a way that cannot
be safely cast to int. |
Unable to statically allocate
keys when Py_LIMITED_API
is defined. |
예제
제안된 변경 사항을 적용하면 TSS 키는 다음과 같이 초기화합니다.:
static Py_tss_t tss_key = Py_tss_NEEDS_INIT;
if (PyThread_tss_create(&tss_key)) {
/* ... handle key creation failure ... */
}
그런 다음 키의 초기화 상태를 다음과 같이 확인할 수 있습니다.:
assert(PyThread_tss_is_created(&tss_key));
나머지 API는 기존 API와 유사한 방식으로 사용합니다.:
int the_value = 1;
if (PyThread_tss_get(&tss_key) == NULL) {
PyThread_tss_set(&tss_key, (void *)&the_value);
assert(PyThread_tss_get(&tss_key) != NULL);
}
/* ... once done with the key ... */
PyThread_tss_delete(&tss_key);
assert(!PyThread_tss_is_created(&tss_key));
Py_LIMITED_API가 정의된 경우 TSS 키를 동적으로 할당해야 합니다.:
static Py_tss_t *ptr_key = PyThread_tss_alloc();
if (ptr_key == NULL) {
/* ... handle key allocation failure ... */
}
assert(!PyThread_tss_is_created(ptr_key));
/* ... once done with the key ... */
PyThread_tss_free(ptr_key);
ptr_key = NULL;
플랫폼 지원 변경 사항
새로운 “Native Thread Implementation” 절이 PEP 11에 추가되어 다음을 명시합니다:
- CPython 3.7부터 모든 플랫폼은 TSS API를 구현하기 위해 pthreads 또는 Windows와 같은 네이티브 스레드 구현을 제공해야 합니다. 네이티브 스레드가 없는 구현에서 발생하는 모든 TSS API 문제는 “수정하지 않음”으로 종료됩니다.
동기
여기서 문제가 되는 주요 사항은 원래 PyThread TLS API에서 정의한 TLS 값에 사용되는 키(int)의 형식입니다.
원래 TLS API는 1997년에 GvR이 Python에 추가했으며, 당시 TLS 값을 나타내는 키는 int였고 현재까지도 그러합니다. 이 API는 CPython 자체의 TLS 구현을 사용했으며, 해당 구현은 오랫동안 사용되지 않은 채 Python/thread.c에 대체로 변경 없이 남아 있었습니다. 네이티브 스레드 구현(pthreads 및 Windows) 위에서 API를 구현하는 기능은 훨씬 나중에 추가되었으며, 내장 구현은 더 이상 필요하지 않은 것으로 판단되어 이후 제거되었습니다 [9].
TLS 키를 나타내는 데 int를 선택한 데 따른 문제는 CPython 자체의 TLS 구현에서는 문제가 없었고 유사한 데이터에 DWORD를 사용하는 Windows와도 우연히 호환되지만, pthreads API에 대한 POSIX 표준과는 호환되지 않는다는 점입니다. POSIX 표준은 pthread_key_t를 표준에서 더 이상 정의하지 않는 불투명한 형식으로 규정합니다(앞에서 설명한 Py_tss_t와 같습니다) [14]. 따라서 pthread_key_t 값이 스레드별 데이터를 조회하는 데 어떻게 사용되는지는 기반 구현에 맡겨집니다.
이는 일반적으로 Python API의 문제가 되지 않았습니다. Linux에서는 pthread_key_t가 unsigned int로 정의되어 Python의 TLS API와 완전히 호환되기 때문입니다. 즉, pthread_create_key로 생성된 pthread_key_t를 int로 자유롭게 변환했다가 다시 되돌릴 수 있습니다(물론 정확히 그러한 것은 아니며, issue #22206에서 지적한 것처럼 이 방식에도 일부 제한이 있습니다).
그러나 issue #25658에서 지적하듯이, Cygwin과 CloudABI를 비롯해(다른 플랫폼도 그럴 가능성이 높습니다) 현대적이고 POSIX를 준수하는 pthreads 구현을 갖추었지만 pthread_key_t가 int로 안전하게 변환될 수 없는 방식으로 정의되어 있어 Python API와 호환되지 않는 플랫폼이 적어도 일부 존재합니다. 실제로 pthreads TLS가 추가될 당시 MvL은 이 문제에 직면할 가능성을 제기했습니다 [2].
새로운, 그 밖에는 공식적으로 지원되지 않는 플랫폼(예: CloudABI)을 지원하기 위한 구체적인 요구사항을 PEP 11이 규정하고 있으며, Cygwin 지원 상태가 현재 불확실하다고 주장할 수 있습니다. 그러나 이로 인해 그 밖에는 Linux 및/또는 POSIX와 호환되고 이 한 가지 장애물만 없다면 CPython이 정상적으로 작동할 수 있는 플랫폼을 지원하는 데 매우 높은 장벽이 생깁니다. CPython 자체가 POSIX와 호환되지 않는 API를 통해 이러한 구현 장벽을 만들고 있으며(실제로 pthreads에 대해 잘못된 가정을 하기도 합니다).
제안된 해결책의 근거
TLS 값을 키로 지정하는 데 불투명한 형식(Py_tss_t)을 사용하면 CPython이 지원하는 현재의 모든 네이티브 TLS 구현(POSIX 및 Windows)과 향후의 네이티브 TLS 구현(C11?)에서 API를 호환되게 사용할 수 있습니다. Py_tss_t의 정의가 기반 구현에 따라 달라질 수 있기 때문입니다.
기존 TLS API가 일부 플랫폼(예: Linux)에서 the limited API [13]로 제공되어 왔으므로, CPython은 새로운 TSS API도 동일한 수준에서 제공하기 위해 노력합니다. 그러나 Py_LIMITED_API가 정의되면 Py_tss_t 정의가 불투명한 구조체가 된다는 점에 유의하십시오. NATIVE_TSS_KEY_T를 제한된 API의 일부로 노출하면 확장 모듈을 다시 빌드하지 않고는 네이티브 스레드 구현을 전환할 수 없기 때문입니다.
하위 호환성을 유지하려면 현재 API의 함수 시그니처를 변경하는 대신 새로운 API를 도입해야 합니다. 또한 새로운 API는 이러한 관련 함수들을 하나의 이름 접두사인 PyThread_tss_ 아래에 더 명확하게 그룹화합니다. 이름의 “tss”는 “thread-specific storage”를 의미하며, C11 스레드 API [15]의 일부인 “tss” API의 명명 및 설계에서 영향을 받았습니다. 그러나 이는 C11 스레드 API와의 호환성이나 지원을 암시하거나 C11을 지원할 향후 의도를 나타내려는 것이 전혀 아닙니다. 단지 명명과 설계에 영향을 준 것일 뿐입니다.
특수 초기화 프로그램 Py_tss_NEEDS_INIT을 포함해야 하는 이유는 모든 네이티브 TLS 구현이 초기화되지 않은 TLS 키에 대한 센티널 값을 정의하는 것은 아니기 때문입니다. 예를 들어 Windows에서는 TLS 키가 DWORD (unsigned int)로 표현되며, 그 값은 불투명한 것으로 취급해야 합니다 [3]. 따라서 Windows에서는 초기화되지 않은 TLS 키를 안전하게 나타내는 데 사용할 수 있는 부호 없는 정수 값이 없습니다. 마찬가지로 POSIX는 초기화되지 않은 pthread_key_t에 대한 센티널을 지정하지 않고, 대신 pthread_once 인터페이스에 의존하여 주어진 TLS 키가 프로세스당 한 번만 초기화되도록 합니다. 따라서 Py_tss_t 형식에는 기반 구현과 무관하게 키의 초기화 상태를 나타낼 수 있는 명시적인 ._is_initialized가 포함되어 있습니다.
sizeof(int) != sizeof(pthread_key_t)인 pthread 사용 시스템에서 PyThread_create_key가 즉시 실패 상태를 반환하도록 변경하는 것은 정상성 검사로 의도된 것입니다: 현재 이러한 시스템에서는 PyThread_create_key가 처음에는 성공했다고 보고할 수 있지만, 반환된 키를 사용하려는 시도는 실패할 가능성이 높습니다. 실제로는 이 실패가 인터프리터 초기화 과정에서 더 일찍 발생하지만, 유효하지 않은 키를 사용하려고 할 때 나중에 실패하는 것보다 문제의 근원인 PyThread_create_key에서 즉시 실패하도록 하는 것이 더 낫습니다. 다시 말해, 이는 기존 API를 안정적으로 사용할 수 없는 플랫폼에서는 지원하지 않으며, 그러한 지원을 추가하려는 노력도 하지 않을 것임을 명확히 나타냅니다.
거부된 아이디어
- 아무것도 하지 않기: Linux에서 작동하므로 현 상태로도 괜찮으며, CPython에서 지원되기를 원하는 플랫폼은 PEP 11의 요구 사항을 따라야 합니다. 위에서 설명한 것처럼, 특정 플랫폼의 특수한 동작이나 기능을 지원하도록 CPython을 변경해 달라는 요청을 받는 경우라면 이는 타당한 주장일 수 있지만, 이 경우에는 그 밖의 POSIX 호환 플랫폼에서 CPython이 잠재력을 최대한 발휘하여 사용되는 것을 막는 CPython 자체의 특이한 동작입니다. 현재 구현이 우연히 Linux에서 작동한다는 사실은 단순한 행운일 뿐이며, 이것이 앞으로도 절대 바뀌지 않으리라는 보장은 없습니다.
- 영향을 받는 플랫폼은 Python을
--without-threads로 구성해야 합니다: 이제--without-threads옵션이 Python 3.7에서 제거되었으므로 더 이상 선택지가 아닙니다 [16]. - 영향을 받는 플랫폼은 네이티브 TLS 구현 대신 CPython에 내장된 TLS 구현을 사용해야 합니다: 이는 앞서의 방안보다 더 받아들일 만한 대안이며, 실제로 바로 그런 작업을 수행하는 패치도 있었습니다 [4]. 하지만 내장 구현이 네이티브 구현보다 일반적으로 “더 느리고 투박”하다는 점은 영향을 받는 플랫폼에서 여전히 불필요하게 성능을 저해합니다. Python이 네이티브 TLS 구현 없이 빌드될 경우 최소 하나의 다른 모듈(
tracemalloc)도 마찬가지로 오동작합니다. 또한 내장 구현이 그 이후로 제거되었기 때문에 이 방안 역시 채택할 수 없습니다. - 기존 API는 유지하되,
pthread_key_t값을int값으로 매핑함으로써 이 문제를 우회합니다. 이를 시도한 사례가 몇 차례 있었지만([5], [6]), 이는 현재 이 문제의 영향을 받지 않는 플랫폼(예: Linux)의 성능에 민감한 코드에 불필요한 복잡성과 오버헤드를 주입합니다. 이 우회 방법의 사용을 플랫폼 호환성에 따라 조건부로 적용하더라도, 유지해야 할 플랫폼별 코드가 추가로 생기며, 영향을 받는 플랫폼에서 불필요하게 성능을 저해한다는 앞서 기각된 방안들의 문제가 여전히 남아 있습니다.
구현
이 문제에 대한 패치의 초기 버전 [7]은 버그 트래커에서 확인할 수 있습니다. GitHub로 이전한 이후로, 개발은 GitHub 상의 CPython 저장소를 포크한 Masayuki Yamamoto의 저장소에 있는 pep539-tss-api 기능 브랜치 [10]에서 계속되고 있습니다. 개발 진행 중인 PR은 [11]에서 확인할 수 있습니다.
이 참조 구현은 새로운 API 구현 기능뿐만 아니라, 기존 TLS API를 새로운 TSS API로 대체하는 데 필요한 클라이언트 코드 업데이트도 포함합니다.
Copyright
This document has been placed in the public domain.