Python의 프리 스레딩 지원
Python의 프리 스레딩 지원 (Free Threading)
3.13 릴리스부터 CPython은 전역 인터프리터 락(GIL)이 꺼진 프리 스레딩(free threading)이라는 빌드를 지원해요. 프리 스레딩 실행은 스레드를 가용 CPU 코어에서 병렬로 돌려서 가용 처리 성능을 최대한 활용하게 해 주죠. 모든 소프트웨어가 자동으로 이점을 얻는 건 아니지만, 스레딩을 염두에 두고 설계된 프로그램은 멀티코어 하드웨어에서 더 빨리 동작해요. 일부 서드파티 패키지, 특히 확장 모듈이 있는 패키지는 프리 스레딩 빌드에서 아직 준비가 안 됐을 수 있고, 그 경우 GIL을 다시 켜게 돼요.
이 문서는 프리 스레딩이 Python 코드에 주는 영향에 대해 설명해요. 프리 스레딩 빌드를 지원하는 C 확장을 어떻게 작성하는지는 'C API 확장의 프리 스레딩 지원'을 참고하세요.
출처: Python 공식 문서
설치
Python 3.13부터 공식 macOS·Windows 설치 프로그램은 프리 스레딩 Python 바이너리 설치를 선택적으로 지원해요. 설치 프로그램은 https://www.python.org/downloads/에서 받을 수 있어요.
다른 플랫폼에 대한 정보는 커뮤니티가 관리하는 '프리 스레딩 Python 설치하기' 가이드를 참고하세요.
CPython을 소스에서 빌드할 때는 --disable-gil configure 옵션을 써서 프리 스레딩 Python 인터프리터를 빌드하면 돼요.
프리 스레딩 Python 식별하기
현재 인터프리터가 프리 스레딩을 지원하는지 확인하려면, python -VV와 sys.version에 'free-threading build'라는 문구가 들어 있는지 보면 돼요. 새로 추가된 sys._is_gil_enabled() 함수는 실행 중인 프로세스에서 GIL이 실제로 꺼졌는지 확인하는 데 쓸 수 있어요.
sysconfig.get_config_var("Py_GIL_DISABLED") 설정 변수는 빌드가 프리 스레딩을 지원하는지 판별하는 데 사용할 수 있어요. 변수가 1로 설정돼 있으면 그 빌드는 프리 스레딩을 지원해요. 이게 빌드 구성과 관련된 결정에 권장되는 메커니즘이에요.
프리 스레딩 Python에서의 전역 인터프리터 락
CPython의 프리 스레딩 빌드는 런타임에 PYTHON_GIL 환경 변수나 -X gil 명령줄 옵션을 써서 GIL을 켠 채로 실행하는 것을 선택적으로 지원해요.
GIL은 프리 스레딩을 지원한다고 명시적으로 표시되지 않은 C-API 확장 모듈을 임포트할 때 자동으로 켜질 수도 있어요. 이 경우 경고가 출력돼요.
개별 패키지 문서 외에도, 다음 웹사이트들이 인기 패키지의 프리 스레딩 지원 현황을 추적해요.
스레드 안전성
CPython의 프리 스레딩 빌드는 Python 레벨에서 기본 GIL 켜짐 빌드와 비슷한 스레드 안전성 동작을 제공하는 것을 목표로 해요. dict, list, set 같은 내장 타입은 내부 락을 써서 GIL과 비슷하게 동작하는 방식으로 동시 수정을 보호해요. 다만 Python은 역사적으로 이런 내장 타입에 대한 동시 수정의 특정 동작을 보장해 온 게 아니에요. 따라서 이건 현재 구현의 설명이지, 현재 또는 미래 동작의 보장이 아니라고 봐야 해요.
참고
가능하다면 내장 타입의 내부 락에 의존하기보다
threading.Lock이나 다른 동기화 프리미티브를 쓰는 게 권장돼요.
알려진 제한 사항
이 절에서는 프리 스레딩 CPython 빌드의 알려진 제한 사항을 설명해요.
불멸화 (Immortalization)
프리 스레딩 빌드에서는 어떤 객체가 불멸(immortal)이 돼요. 불멸 객체는 할당 해제되지 않고 절대 수정되지 않는 참조 카운트를 가져요. 이는 효율적인 멀티스레드 확장을 막을 참조 카운트 경쟁을 피하기 위한 거예요.
3.14 릴리스 기준으로 불멸화는 다음으로 제한돼요:
- 코드 상수: 숫자 리터럴, 문자열 리터럴, 그리고 다른 상수로 이루어진 튜플 리터럴.
sys.intern()으로 인터닝된 문자열.
프레임 객체
다른 스레드에서 현재 실행 중인 프레임의 frame.f_locals를 그 프레임 객체에서 접근하는 건 안전하지 않아요. 접근하면 인터프리터가 크래시할 수 있죠.
이터레이터
여러 스레드에서 동시에 같은 이터레이터 객체에 접근하는 것은 일반적으로 스레드에 안전하지 않아요. 이터레이터를 쓰는 스레드들은 중복되거나 빠진 요소를 볼 수 있어요.
단일 스레드 성능
프리 스레딩 빌드는 기본 GIL 켜짐 빌드에 비해 Python 코드를 실행할 때 추가 오버헤드가 있어요. 오버헤드 양은 워크로드와 하드웨어에 따라 달라져요. pyperformance 벤치마크 세트에서는 평균 오버헤드가 macOS aarch64에서 약 1%, x86-64 Linux 시스템에서 8% 정도까지 돼요.
동작 변경 사항
이 절에서는 프리 스레딩 빌드에서의 CPython 동작 변경을 설명해요.
컨텍스트 변수
프리 스레딩 빌드에서는 thread_inherit_context 플래그가 기본적으로 참(true)으로 설정돼요. 그래서 threading.Thread로 만든 스레드는 start()를 호출한 쪽의 Context() 복사본으로 시작해요. 기본 GIL 켜짐 빌드에서는 이 플래그가 기본적으로 거짓(false)이라 빈 Context()로 시작해요.
경고 필터
프리 스레딩 빌드에서는 context_aware_warnings 플래그가 기본적으로 참으로 설정돼요. 기본 GIL 켜짐 빌드에서는 이 플래그가 기본적으로 거짓이에요. 플래그가 참이면 warnings.catch_warnings 컨텍스트 매니저가 경고 필터에 컨텍스트 변수를 사용해요. 거짓이면 catch_warnings가 전역 필터 리스트를 수정하는데, 그건 스레드 안전하지 않아요. 자세한 내용은 warnings 모듈을 참고하세요.
메모리 사용 증가
프리 스레딩 빌드는 기본 빌드에 비해 보통 더 많은 메모리를 사용해요. 여기에는 여러 이유가 있는데, 대부분 설계 결정 때문이에요.
모든 인터닝 문자열이 불멸
현대 Python 버전(버전 2.3 이후)에서는 문자열을 인터닝해도(예: sys.intern()으로) 그 문자열이 불멸이 되진 않아요. 대신 그 문자열의 마지막 참조가 사라지면 인터닝 문자열 테이블에서 제거돼요. 프리 스레딩 빌드에서는 그렇지 않아요. 인터닝된 문자열은 불멸이 되어 인터프리터 종료까지 살아남아요.
GC가 아닌 객체는 더 큰 객체 헤더를 가져요
프리 스레딩 빌드는 다른 PyObject 구조를 사용해요. 기본 빌드처럼 PyObject 구조 앞에 GC 관련 정보를 할당하는 대신, GC 관련 정보가 일반 객체 헤더의 일부가 돼요. 예를 들어 AMD64 플랫폼에서 None은 프리 스레딩 빌드에서는 32바이트, 기본 빌드에서는 16바이트를 사용해요. GC 객체(예: dict, list)는 두 빌드 모두 같은 크기인데, 프리 스레딩 빌드는 GC 정보에 추가 공간을 쓰지 않기 때문이에요.
QSBR이 메모리 해제를 지연시킬 수 있어요
락 프리(lock-free) 데이터 구조를 안전하게 구현하려고, 대기 상태 기반 회수(QSBR, quiescent state-based reclamation)라는 안전 메모리 회수(SMR) 방식이 사용돼요. 이는 락 프리 접근을 허용하는 데이터 구조를 뒷받침하는 메모리가 QSBR을 사용해 즉시 해제하는 대신 free 연산을 연기한다는 뜻이에요. 이런 데이터 구조의 두 예가 list 객체와 딕셔너리 키 객체예요. QSBR이 어떻게 구현되는지에 대한 자세한 내용은 CPython 소스 트리의 InternalDocs/qsbr.md를 참고하세요. gc.collect()를 실행하면 QSBR이 붙잡고 있는 모든 메모리가 실제로 해제되게 해요. QSBR이 메모리를 해제해도 기본 메모리 할당자가 그 메모리를 OS에 즉시 되돌리지 않을 수 있으므로, 프로세스의 상주 세트 크기(RSS)가 줄어들지 않을 수 있음을 참고하세요.
mimalloc 할당자 vs pymalloc
기본 빌드는 작은 할당(512바이트 이하)에 보통 'pymalloc' 메모리 할당자를 사용해요. 프리 스레딩 빌드는 pymalloc을 쓰지 않고 모든 Python 객체를 'mimalloc' 할당자로 할당해요. pymalloc 할당자는 메모리 사용을 낮게 유지하는 데 도움을 주는 다음 성질들이 있어요: 블록당 작은 오버헤드, 효과적인 메모리 단편화 방지, 해제된 메모리를 운영 체제로 빠르게 반환. mimalloc 할당자도 이 면에서 꽤 잘하지만 오버헤드가 조금 더 있을 수 있어요.
프리 스레딩 빌드에서는 mimalloc이 메모리를 여러 개의 분리된 힙(현재 네 개)으로 관리해요. 예를 들어 GC를 지원하는 모든 객체는 자기 고유의 힙에서 할당되죠. 분리된 힙을 사용한다는 건 한 힙의 빈 메모리를 다른 힙을 쓰는 할당에 쓸 수 없다는 뜻이에요. 또한 어떤 힙은 그 힙을 뒷받침하는 메모리(mimalloc 용어로 '페이지'라고 함)를 해제할 때 QSBR을 쓰도록 설정돼 있어요. QSBR을 쓰면 페이지의 모든 메모리 블록이 해제된 시점과 메모리 페이지가 새 할당이나 OS로 방출되는 시점 사이에 지연이 생겨요.
mimalloc 할당자는 또한 해제된 메모리를 OS로 되돌리는 것을 연기해요. MIMALLOC_PURGE_DELAY 환경 변수를 0으로 설정하면 그 지연을 줄일 수 있어요. 다만 이렇게 하면 아마 할당자 성능이 떨어질 거예요.
프리 스레딩 참조 카운팅은 객체가 더 오래 살게 할 수 있어요
기본 빌드에서는 객체의 참조 카운트가 0에 도달하면 보통 할당 해제돼요. 프리 스레딩 빌드는 '편향된 참조 카운팅'(biased reference counting)을 사용하는데, 현재 스레드가 '소유한' 객체에는 빠른 경로를, 다른 객체에는 느린 경로를 써요. 자세한 내용은 PEP 703을 참고하세요. 객체의 참조 카운트가 '대기'(queued) 상태에 들어갈 때마다 할당 해제가 연기될 수 있어요. 대기 상태는 바이트코드 평가기의 'eval breaker' 구간에서 해소돼요.
프리 스레딩 빌드는 '지연된 참조 카운팅'(deferred reference counting)이라는 다른 참조 카운팅 모드도 허용해요. 이 모드는 객체 단위로 플래그를 설정해서 켜요. 지연 참조 카운팅은 다음 타입에 대해 활성화돼요:
- 모듈 객체
- 모듈 최상위 함수
- 클래스 스코프에 정의된 클래스 메서드
- 디스크립터 객체
threading.local로 만든 스레드 로컬 객체
지연 참조 카운팅이 켜지면 Python 함수 스택에서 오는 참조는 참조 카운트에 더해지지 않아요. 이 방식은 여러 스레드에서 쓰이는 객체에서 특히 참조 카운팅 오버헤드를 줄여 줘요. 스택 참조가 계산되지 않으므로, 지연 참조 카운팅을 쓰는 객체는 내부 참조 카운트가 0이 돼도 즉시 해제되지 않아요. 대신 다음 GC 실행에서 검사되고, 그 객체에 대한 스택 참조가 없다고 판단되면 해제돼요. 즉 이 객체들은 일반적으로 그렇듯 참조 카운트가 0이 될 때가 아니라 GC에 의해 해제돼요.
스레드별 참조 카운팅은 객체 해제를 지연시킬 수 있어요
자주 공유되는 객체의 참조 카운트 필드에 대한 경쟁을 피하려고, 프리 스레딩 빌드는 몇 가지 선별된 객체 타입에 '스레드별 참조 카운팅'(per-thread reference counting)을 써요. 하나의 공유 참조 카운트를 갱신하는 대신, 각 스레드가 객체에 부여된 고유 id로 인덱싱된 자기 자신의 로컬 참조 카운트 배열을 유지해요. 진짜 참조 카운트는 객체의 로컬 카운트가 0으로 떨어질 때만 스레드별 카운트를 합산해서 계산돼요. 스레드별 참조 카운팅은 현재 다음에 사용돼요:
- 힙 타입 객체(Python으로 만든 클래스)
- 코드 객체
- 모듈 객체의
__dict__
스레드별 카운트는 해제되기 전에 객체로 다시 합쳐져야 하므로, 스레드별 참조 카운팅을 쓰는 객체는 기본 빌드에서보다 보통 더 늦게 해제돼요. 특히 그런 객체는 보통 그 객체를 참조했던 스레드가 안전 지점(예: 바이트코드 평가기의 'eval breaker' 구간)에 도달하거나 종료할 때까지 해제되지 않아요. gc.collect()를 실행하면 스레드별 카운트를 합치고 이 객체들이 해제되게 해요.
더 알아보기 (Learn more)
- 프리 스레딩 Python 전체를 개괄한 PEP 703 – Making the Global Interpreter Lock Optional in CPython을 읽어 보세요.
- 프리 스레딩 빌드를 지원하는 C 확장 작성법은 C API 확장의 프리 스레딩 지원 문서를 참고하세요.