무료 스레딩(free threading)에 대한 Python 지원¶
Starting with the 3.13 release, CPython has support for a build of Python called free threading where the global interpreter lock (GIL) is disabled. Free-threaded execution allows for full utilization of the available processing power by running threads in parallel on available CPU cores. While not all software will benefit from this automatically, programs designed with threading in mind will run faster on multi-core hardware.
Some third-party packages, in particular ones with an extension module, may not be ready for use in a free-threaded build, and will re-enable the GIL.
이 문서는 Python 코드에 대한 자유 스레딩의 영향에 대해 설명합니다. 자유 스레딩 빌드를 지원하는 C 확장을 작성하는 방법에 대한 정보는 Free Threading을 위한 C API 확장 지원 를 참조하십시오.
더 보기
PEP 703 – Making the Global Interpreter Lock Optional in CPython. 자유 스레딩 Python에 대한 전체적인 설명을 제공합니다.
설치 방법¶
Python 3.13부터 공식 macOS 및 Windows 설치 프로그램은 선택적으로 자유 스레딩(free-threaded) Python 바이너리를 설치하는 것을 지원합니다. 설치 프로그램은 https://www.python.org/downloads/에서 확인할 수 있습니다.
다른 플랫폼에 대한 정보는 커뮤니티에서 관리하는 설치 가이드인 Installing a Free-Threaded Python 를 참조하십시오.
소스에서 CPython을 빌드할 때, 자유 스레딩 Python 인터프리터를 구축하려면 --disable-gil 구성 옵션을 사용해야 합니다.
자유 스레딩 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을 활성화하여 실행하는 것을 지원합니다.
자유 스레딩을 지원한다고 명시적으로 표시되지 않은 C-API 확장 모듈을 임포트할 때, GIL이 자동으로 활성화될 수 있습니다. 이 경우 경고 메시지가 출력됩니다.
개별 패키지 문서 외에도 다음 웹사이트에서 인기 있는 패키지들의 자유 스레딩 지원 상태를 추적합니다:
스레드 안전성¶
CPython의 free-threaded 빌드는 파이썬 수준에서 기본 GIL 활성화 빌드와 유사한 스레드 안전성 동작을 제공하는 것을 목표로 합니다. dict, list, 그리고 set 과 같은 내장형 타입은 GIL과 유사하게 동작하는 방식으로 동시 수정을 방지하기 위해 내부 잠금(internal locks)을 사용합니다. 그러나 파이썬은 역사적으로 이러한 내장형 타입의 동시 수정에 대해 특정 동작을 보장해 온 적이 없으므로, 이는 현재 구현에 대한 설명으로 간주해야 하며 현재 또는 미래의 동작에 대한 보증으로 해석해서는 안 됩니다.
참고
가능한 경우 내장형 타입의 내부 잠금에 의존하는 대신 threading.Lock 또는 다른 동기화 기본형을 사용하는 것이 좋습니다.
알려진 한계¶
이 섹션에서는 free-threaded CPython 빌드의 알려진 한계에 대해 설명합니다.
불멸화(Immortalization)¶
In the free-threaded build, some objects are immortal. Immortal objects are not deallocated and have reference counts that are never modified. This is done to avoid reference count contention that would prevent efficient multi-threaded scaling.
3.14 릴리스를 기준으로, 불멸화는 다음 범위로 제한됩니다:
코드 상수: 수치 리터럴, 문자열 리터럴, 그리고 다른 상성들로 구성된 튜플 리터럴.
sys.intern()에 의해 인턴된 문자열.
프레임 객체¶
frame 객체의 frame.f_locals 를 해당 프레임이 현재 다른 스레드에서 실행 중일 때 접근하는 것은 안전하지 않으며, 그렇게 하면 인터프리터가 충돌할 수 있습니다.
이터레이터¶
동일한 이터레이터 객체에 여러 스레드가 동시에 접근하는 것은 일반적으로 스레드 안전하지 않으며, 스레드가 중복된 요소를 보거나 누락된 요소를 볼 수 있습니다.
단일 스레드 성능¶
free-threaded 빌드는 기본 GIL 활성화 빌드에 비해 파이썬 코드를 실행할 때 추가적인 오버헤드가 발생합니다. 오버헤드의 양은 작업 부하와 하드웨어에 따라 다릅니다. pyperformance 벤치마크 제품군에서 평균 오버헤드는 macOS aarch64 시스템에서 약 1%에서 x86-64 Linux 시스템에서 8% 사이입니다.
동작 변경 사항¶
이 섹션에서는 free-threaded 빌드에 따른 CPython의 동작 변화를 설명합니다.
컨텍스트 변수¶
free-threaded 빌드에서는 thread_inherit_context 플래그가 기본적으로 true로 설정되어, threading.Thread 로 생성된 스레드가 start() 를 호출한 자의 Context() 복사본과 함께 시작됩니다. 기본 GIL 활성화 빌드에서는 이 플래그가 기본값으로 false이므로 스레드가 비어 있는 Context() 와 함께 시작됩니다.
경고 필터¶
free-threaded 빌드에서는 context_aware_warnings 플래그가 기본적으로 true로 설정됩니다. 기본 GIL 활성화 빌드에서는 이 플래그의 기본값은 false입니다. 해당 플래그가 true이면 warnings.catch_warnings 컨텍스트 관리자가 경고 필터를 위해 컨텍스트 변수를 사용합니다. 만약 플래그가 false라면 catch_warnings 는 스레드 안전하지 않은 전역 필터 목록을 수정합니다. 자세한 내용은 warnings 모듈을 참조하십시오.
증가된 메모리 사용량¶
free-threaded 빌드는 일반적으로 기본 빌드에 비해 더 많은 메모리를 사용합니다. 여기에는 여러 이유가 있으며, 대부분 설계 결정으로 인한 것입니다.
모든 인턴된 문자열은 불멸입니다.¶
최신 파이썬 버전(2.3 버전 이후)에서는 문자열을 인턴하는 것(예: sys.intern() 사용)이 해당 문자열을 불멸 상태로 만들지 않습니다. 대신, 해당 문자열에 대한 마지막 참조가 사라지면 인턴된 문자열 테이블에서 제거됩니다. free-threaded 빌드에서는 그렇지 않으며 모든 인턴된 문자열은 불멸이 되어 인터프리터 종료 시까지 유지됩니다.
비(非) GC 객체는 더 큰 객체 헤더를 가집니다.¶
free-threaded 빌드는 다른 PyObject 구조를 사용합니다. 기본 빌드처럼 PyObject 구조 앞에 GC 관련 정보를 할당하는 대신, GC 관련 정보가 일반 객체 헤더의 일부로 포함됩니다. 예를 들어, AMD64 플랫폼에서 None 은 free-threaded 빌드에서 32바이트를 사용하는 반면, 기본 빌드에서는 16바이트를 사용합니다. GC 객체(딕셔너리 및 리스트 등)는 free-threaded 빌드가 GC 정보를 위한 추가 공간을 사용하지 않으므로 두 빌드에서 동일한 크기를 가집니다.
QSBR로 인해 메모리 해제가 지연될 수 있음¶
락 프리(lock-free) 데이터 구조를 안전하게 구현하기 위해 quiescent state-based reclamation(QSBR)으로 알려진 안전한 메모리 회수(SMR) 방식이 사용됩니다. 이는 락 프리 접근을 허용하는 데이터 구조의 기반 메모리가 즉시 해제되지 않고, 해제 작업을 지연시키는 QSBR을 사용함을 의미합니다. 이러한 데이터 구조의 두 가지 예는 리스트 객체와 딕셔너리 키 객체입니다. QSBR 구현에 대한 자세한 내용은 CPython 소스 트리의 Internal8Docs/qsbr.md 를 참조하십시오. gc.collect() 를 실행하면 QSBR에 의해 유지되는 모든 메모리가 실제로 해제되어야 합니다. 단, QSBR이 메모리를 해제하더라도 하위 메모리 할당자가 즉시 해당 메모리를 OS에 반환하지 않을 수 있으므로 프로세스의 상주 세트 크기(RSS)가 줄어들지 않을 수 있음에 유의하십시오.
mimalloc 할당자 대 pymalloc¶
기본 빌드는 일반적으로 작은 할당(512바이트 이하)에 “pymalloc” 메모리 할당자를 사용합니다. free-threaded 빌드는 pymalloc을 사용하지 않고 모든 파이썬 객체를 “mimalloc” 할당자를 사용하여 할당합니다. pymalloc 할당자는 낮은 메모리 사용량을 유지하는 데 도움이 되는 다음과 같은 특성을 가집니다: 할당 블록당 적은 오버헤드, 효과적인 메모리 단편화 방지, 운영체제에 대한 빠른 여분 메모리 반환. mimalloc 할당자도 이러한 측면에서 성능이 우수하지만 일부 더 많은 오버헤드가 발생할 수 있습니다.
free-threaded 빌드에서 mimalloc은 여러 개의 별도 힙(현재 4개)으로 메모리를 관리합니다. 예를 들어, 모든 GC 지원 객체는 전용 힙에서 할당됩니다. 별도의 힙을 사용한다는 것은 한 힙의 여분 메모리가 다른 힙을 사용하는 할당에 사용될 수 없음을 의미합니다. 또한 일부 힙은 힙을 뒷받침하는 메모리(mimalloc 용어로는 “페이지”라고 함)를 해제할 때 QSBR(quiescent-state based reclamation)을 사용하도록 구성됩니다. QSBR의 사용으로 인해 페이지에 대한 모든 메모리 블록이 해제되는 시점과 해당 메모리 페이지가 새 할당이나 OS로 반환되기 위해 방출되는 시점 사이에 지연이 발생합니다.
mimalloc 할당자 또한 해제된 메모리를 OS에 반환하는 것을 지연합니다. 환경 변수 MIMALLOC_PURGE_DELAY 를 0 으로 설정하여 이 지연을 줄일 수 있습니다. 단, 이로 인해 할당자의 성능이 저하될 가능성이 높으므로 주의하십시오.
free-threaded 참조 횟수 추적으로 인해 객체가 더 오래 유지될 수 있음¶
기본 빌드에서는 객체의 참조 횟수가 0에 도달하면 보통 해제됩니다. free-threaded 빌드는 “편향된 참조 횟수(biased reference counting)”를 사용하며, 현재 스레드가 “소유한” 객체에 대해서는 빠른 경로(fast-path)를, 다른 객체에 대해서는 느린 경로(slow path)를 사용합니다. 자세한 내용은 PEP 703 을 참조하십시오. 객체의 참조 횟수가 “대기(queued)” 상태가 될 때마다 해제 작업이 지연될 수 있습니다. 대기 상태는 바이트 코드 평가기의 “eval breaker” 섹션에서 해제됩니다.
free-threaded 빌드는 또한 “지연된 참조 횟수(deferred reference counting)”라고 알려진 다른 방식의 참조 횟수 추적을 지원합니다. 이 모드는 객체별로 플래그를 설정하여 활성화됩니다. 지연된 참조 횟수는 다음 유형에 대해 활성화됩니다:
모듈 객체
모듈 최상위 함수
클래스 범위 내에 정의된 클래스 메서드
디스크립터 객체
threading.local에 의해 생성된 스레드 로컬 객체
지연된 참조 횟수가 활성화되면 파이썬 함수 스택의 참조가 참조 횟수에 추가되지 않습니다. 이 방식은 특히 여러 스레드에서 사용되는 객체에 대한 참조 횟수 추소 오버헤드를 줄여줍니다. 스택 참조가 계산되지 않기 때문에 지연된 참조 횟수가 적용된 객체는 내부 참조 횟수가 0이 되어도 즉시 해제되지 않습니다. 대신, 다음 GC 실행 시 검사되며, 스택 참조가 발견되지 않는 경우에만 해제됩니다. 이는 이러한 객체가 일반적인 방식과 달리 참조 횟수가 0이 될 때가 아니라 GC에 의해 해제됨을 의미합니다.
스레드별 참조 횟수 추적(Per-thread reference counting)으로 인해 객체 해제가 지연될 수 있음¶
자주 공유되는 객체의 참조 횟수 필드에서 발생하는 경합을 방지하기 위해, free-threaded 빌드는 몇 가지 선택된 객체 유형에 대해 “스레드별 참조 횟수(per-thread reference counting)”를 사용합니다. 단일 공유 참조 횟수를 업데이트하는 대신, 각 스레드는 객체에 할당된 고유 ID로 인덱싱되는 자체 로컬 참조 횟수 배열을 유지합니다. 실제 참조 횟수는 객체의 로컬 카운트가 0으로 떨어질 때 스레드별 카운트를 합산하여 계산됩니다. 현재 스레드별 참조 횟수가 사용되는 대상은 다음과 같습니다:
힙 타입 객체(파이썬에서 생성된 클래스)
코드 객체
모듈 객체의
__dict__
스레드별 카운트를 해제 전 객체에 다시 병합해야 하므로, 스레드별 참조 횟수를 사용하는 객체는 일반적으로 기본 빌드에서보다 나중에 해제됩니다. 특히 이러한 객체는 이를 참조한 스레드가 안전 지점(예: 바이트 코드 평가기의 “eval breaker” 섹션)에 도달하거나 종료될 때까지 해제되지 않습니다. gc.collect() 를 실행하면 스레드별 카운트가 병합되어 이러한 객체들이 해제될 수 있습니다.