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 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 CPython에 새로운 스레드 로컬 저장소(Thread Local Storage, TLS) API를 추가할 것을 제안합니다. 새 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”는 스레드 로컬 저장소(Thread Local Storage)를 뜻합니다. “전송 계층 보안(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;
플랫폼 지원 변경 사항
PEP 11에 다음과 같은 내용의 새로운 “Native Thread Implementation”(네이티브 스레드 구현) 섹션이 추가됩니다:
- CPython 3.7부터 모든 플랫폼은 TSS API를 구현하기 위해 네이티브 스레드 구현(pthreads 또는 Windows 등)을 제공해야 합니다. 네이티브 스레드가 없는 구현에서 발생하는 모든 TSS API 문제는 “won’t fix”(수정하지 않음)로 종료됩니다.
동기
여기서 다루는 주요 문제는 원래의 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와도 우연히 호환되지만, pthread_key_t를 표준에서 더 이상 정의하지 않는 불투명 타입으로 정의하는(위에서 설명한 Py_tss_t와 마찬가지로) pthreads API의 POSIX 표준과는 호환되지 않는다는 점입니다 [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에서 지적하듯이, 그 밖의 면에서는 현대적이고 POSIX를 준수하는 pthreads 구현을 갖추었지만 pthread_key_t가 int로 안전하게 캐스트할 수 없는 방식으로 정의되어 있어 Python API와 호환되지 않는 플랫폼이 적어도 몇 개(바로 Cygwin과 CloudABI이며, 다른 플랫폼도 있을 가능성이 큽니다) 존재합니다. 사실 이 문제에 부딪힐 가능성은 pthreads TLS가 추가될 당시 MvL이 제기한 바 있습니다 [2].
PEP 11이 공식적으로 지원되지 않는 새로운 플랫폼(CloudABI 등)을 지원하기 위한 구체적인 요구 사항을 두고 있으며, Cygwin 지원 상태는 현재 불확실하다고 주장할 수도 있습니다. 그러나 이는 그 밖의 면에서는 Linux 및/또는 POSIX와 호환되며 이 한 가지 장애물만 없다면 CPython이 “그냥 동작”할 수도 있는 플랫폼을 지원하는 데 매우 높은 장벽이 됩니다. CPython 자체가 POSIX와 호환되지 않는 (그리고 실제로 pthreads에 대해 잘못된 가정을 하는) API를 통해 이러한 구현 장벽을 부과하고 있는 것입니다.
제안된 해법의 근거
TLS 값의 키로 불투명 타입(Py_tss_t)을 사용하면 Py_tss_t의 정의가 하부 구현에 따라 달라질 수 있으므로, API가 CPython이 지원하는 현재(POSIX 및 Windows)와 미래(C11?)의 모든 네이티브 TLS 구현과 호환될 수 있습니다.
기존 TLS API는 일부 플랫폼(예: Linux)에서 제한된 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)인 pthreads 사용 시스템에서 PyThread_create_key가 곧바로 실패 상태를 반환하도록 바꾸는 것은 온전성 검사를 위한 것입니다. 현재 이러한 시스템에서는 PyThread_create_key가 처음에는 성공했다고 보고할 수 있지만, 반환된 키를 사용하려고 하면 실패할 가능성이 높습니다. 실제로는 이 실패가 인터프리터 초기화 과정의 이른 시점에 일어나기는 하지만, 나중에 유효하지 않은 키를 사용하려고 할 때 실패하기보다는 문제의 근원(PyThread_create_key)에서 곧바로 실패하는 편이 낫습니다. 다시 말해, 이는 기존 API를 안정적으로 사용할 수 없는 플랫폼에서는 해당 API를 지원하지 않으며, 그러한 지원을 추가하려는 노력도 하지 않을 것임을 분명히 보여 줍니다.
기각된 아이디어
- 아무것도 하지 않기: Linux에서 동작하므로 현재 상태로도 문제가 없으며, CPython의 지원을 받고자 하는 플랫폼은 PEP 11의 요구 사항을 따라야 합니다. 앞서 설명했듯이, CPython에 특정 플랫폼의 특정한 특이점이나 기능을 지원하기 위해 변경을 요청하는 경우라면 이는 타당한 인자가 되겠지만, 이 경우에는 CPython 자체의 특이점 때문에 그 외에는 POSIX를 준수하는 플랫폼에서 CPython의 잠재력을 온전히 활용하지 못하는 것입니다. 현재 구현이 Linux에서 동작하는 것은 운 좋은 우연일 뿐이며, 이것이 앞으로도 절대 바뀌지 않으리라는 보장은 없습니다.
- 영향을 받는 플랫폼에서는 그냥 Python을
--without-threads로 구성하면 됩니다: Python 3.7에서--without-threads옵션이 제거되었으므로 이는 더 이상 선택지가 아닙니다 [16]. - 영향을 받는 플랫폼에서는 네이티브 TLS 구현 대신 CPython의 내장 TLS 구현을 사용해야 합니다: 이는 앞의 아이디어보다 더 받아들일 만한 대안이며, 실제로 바로 그렇게 하는 패치가 있었습니다 [4]. 그러나 내장 구현은 일반적으로 네이티브 구현보다 “더 느리고 투박”하므로, 영향을 받는 플랫폼의 성능을 여전히 불필요하게 떨어뜨립니다. 또한 Python을 네이티브 TLS 구현 없이 빌드하면 적어도 하나의 다른 모듈(
tracemalloc)도 제대로 동작하지 않습니다. 게다가 내장 구현이 그 뒤로 제거되었기 때문에 이 아이디어는 채택할 수도 없습니다. - 기존 API는 유지하되,
pthread_key_t값을int값으로 대응시키는 매핑을 제공하여 문제를 우회합니다. 이를 위한 시도가 몇 차례 있었지만([5], [6]), 이는 현재 이 문제의 영향을 받지 않는 플랫폼(예: Linux)의 성능이 중요한 코드에 불필요한 복잡성과 오버헤드를 끌어들입니다. 이 우회 방법을 플랫폼 호환성에 따라 조건부로 사용하도록 하더라도 유지 보수해야 할 플랫폼별 코드가 생기며, 영향을 받는 플랫폼의 성능을 불필요하게 떨어뜨린다는 앞서 기각된 아이디어들의 문제도 여전히 남습니다.
구현
이 이슈에 대한 패치의 초기 버전 [7]은 버그 트래커에서 확인할 수 있습니다. GitHub으로 이전한 이후, 개발은 GitHub에 있는 Masayuki Yamamoto의 CPython 저장소 포크의 pep539-tss-api 기능 브랜치 [10]에서 계속되었습니다. 작업 중인 PR은 [11]에서 확인할 수 있습니다.
이 참조 구현은 새 API의 구현 기능뿐만 아니라, 기존 TLS API를 새 TSS API로 교체하는 데 필요한 클라이언트 코드 업데이트도 포함합니다.
Copyright
This document has been placed in the public domain.