Python 3.13의 free-threaded 모드를 심층 분석합니다. GIL의 역사와 문제점, PEP 703의 설계, free-threaded 빌드의 설치와 실전 멀티스레드 성능을 다룹니다.
GIL(Global Interpreter Lock)은 CPython의 가장 유명한 제약입니다. GIL은 한 시점에 하나의 스레드만 Python 바이트코드를 실행할 수 있도록 보장하는 뮤텍스(mutex)입니다.
CPython은 참조 카운팅으로 메모리를 관리합니다. 모든 객체에는 참조 횟수를 추적하는 카운터가 있으며, 이 카운터가 0이 되면 객체가 해제됩니다. 문제는 이 카운터가 스레드 안전하지 않다는 것입니다.
스레드 A 스레드 B
----------- -----------
obj.refcount = 1 obj.refcount = 1
x = obj (refcount++ -> 2) y = obj (refcount++ -> ???)
# 두 스레드가 동시에 refcount를 증가시키면
# 2가 아닌 다른 값이 될 수 있음 (경쟁 조건)GIL은 이 문제를 가장 단순한 방법으로 해결합니다. 한 번에 하나의 스레드만 실행하면 경쟁 조건이 발생하지 않습니다. 이 설계 덕분에 CPython의 C 확장 작성이 단순해지고, 단일 스레드 성능이 최적화되었습니다.
GIL의 대가는 명확합니다. CPU 바운드 작업에서 멀티스레딩이 무의미합니다.
import threading
import time
def cpu_bound(n: int) -> int:
"""CPU 집약적 작업"""
total = 0
for i in range(n):
total += i * i
return total
N = 50_000_000
# 순차 실행
start = time.time()
cpu_bound(N)
cpu_bound(N)
sequential_time = time.time() - start
# 멀티스레드 실행 (GIL 있음)
start = time.time()
t1 = threading.Thread(target=cpu_bound, args=(N,))
t2 = threading.Thread(target=cpu_bound, args=(N,))
t1.start()
t2.start()
t1.join()
t2.join()
threaded_time = time.time() - start
print("Sequential: " + str(round(sequential_time, 2)) + "s")
print("Threaded: " + str(round(threaded_time, 2)) + "s")
# GIL이 있으면 threaded_time >= sequential_timeGIL이 있는 상태에서는 두 스레드가 번갈아가며 실행되므로, 멀티스레드 버전이 순차 실행보다 오히려 느릴 수 있습니다. 컨텍스트 스위칭 오버헤드까지 더해지기 때문입니다.
PEP 703은 Sam Gross가 제안한 것으로, CPython에서 GIL을 선택적으로 비활성화할 수 있도록 합니다. Python 3.13에서 실험적으로 도입되었습니다.
PEP 703의 핵심 설계 원칙은 다음과 같습니다.
1. 기존 코드 호환성 유지
- GIL 활성 빌드가 기본
- free-threaded 빌드는 별도 실행 파일
2. 단일 스레드 성능 최소 저하
- 목표: 5% 이내 오버헤드
- 실측: 1~8% (플랫폼에 따라 다름)
3. 점진적 전환
- C 확장 라이브러리의 호환성 경로 제공
- 라이브러리가 준비되면 free-threaded 지원 선언PEP 703은 GIL 없이 참조 카운팅을 안전하게 만들기 위해 여러 기법을 도입합니다.
1. Biased Reference Counting
- 객체를 생성한 스레드(소유 스레드)는 빠른 경로 사용
- 다른 스레드는 원자적 연산으로 참조 카운팅
- 대부분의 객체는 생성 스레드에서 사용되므로 오버헤드 최소
2. Deferred Reference Counting
- 지역 변수 같은 단기 참조는 카운팅을 지연
- 가비지 컬렉션 시점에 일괄 처리
3. Immortal Objects (PEP 683)
- None, True, False 등은 참조 카운팅 자체를 하지 않음
- 6장에서 다룬 내용
4. Per-object Locks
- 객체별 세밀한 잠금으로 동시 접근 제어
- dict, list 등 컨테이너 객체에 적용Free-threaded Python은 별도의 빌드입니다. 공식 설치 프로그램에서 옵션으로 선택하거나, 소스에서 빌드할 수 있습니다.
# 공식 설치 프로그램 사용 시 "Free-threaded" 옵션 체크
# 설치 후 별도 실행 파일 생성
python3.13t # free-threaded 빌드
# 또는 소스에서 빌드
./configure --disable-gil
make
make install# uv를 사용하면 더 간편함
uv python install 3.13t
uv run --python 3.13t script.py# 공식 설치 프로그램에서 "Customize installation" 선택
# "Free-threaded binaries" 옵션 체크
# 설치 후: python3.13t.exeimport sys
# GIL 상태 확인
print(sys._is_gil_enabled())
# False -> free-threaded 모드
# True -> GIL 활성 모드free-threaded 빌드는 실험적 기능입니다. 프로덕션 환경에서의 사용은 아직 권장되지 않습니다. 일부 C 확장 라이브러리가 호환되지 않을 수 있으며, 예상치 못한 버그가 발생할 수 있습니다.
Free-threaded 빌드에서 실제 멀티스레드 성능이 어떻게 변하는지 테스트합니다.
import threading
import time
import sys
def compute_sum(start: int, end: int, results: list, index: int) -> None:
"""지정된 범위의 제곱합을 계산"""
total = 0
for i in range(start, end):
total += i * i
results[index] = total
def run_sequential(n: int, num_chunks: int) -> float:
chunk_size = n // num_chunks
results = [0] * num_chunks
start_time = time.time()
for i in range(num_chunks):
start = i * chunk_size
end = start + chunk_size
compute_sum(start, end, results, i)
return time.time() - start_time
def run_threaded(n: int, num_threads: int) -> float:
chunk_size = n // num_threads
results = [0] * num_threads
threads = []
start_time = time.time()
for i in range(num_threads):
start = i * chunk_size
end = start + chunk_size
t = threading.Thread(
target=compute_sum, args=(start, end, results, i)
)
threads.append(t)
t.start()
for t in threads:
t.join()
return time.time() - start_time
N = 50_000_000
NUM_THREADS = 4
seq_time = run_sequential(N, NUM_THREADS)
thr_time = run_threaded(N, NUM_THREADS)
print("GIL enabled: " + str(sys._is_gil_enabled()))
print("Sequential: " + str(round(seq_time, 3)) + "s")
print("Threaded (" + str(NUM_THREADS) + "): " + str(round(thr_time, 3)) + "s")
print("Speedup: " + str(round(seq_time / thr_time, 2)) + "x")# GIL 활성 빌드 (python3.13)
GIL enabled: True
Sequential: 4.521s
Threaded (4): 4.892s
Speedup: 0.92x # GIL로 인해 오히려 느림
# Free-threaded 빌드 (python3.13t)
GIL enabled: False
Sequential: 4.873s # 단일 스레드에서 약간의 오버헤드
Threaded (4): 1.384s
Speedup: 3.52x # 거의 4배 가까운 병렬화I/O 바운드 작업에서는 GIL이 I/O 대기 중에 해제되므로, 기존에도 멀티스레딩의 효과가 있었습니다. Free-threaded 모드는 CPU 바운드 작업에서의 변화가 핵심입니다.
CPU 바운드 작업:
GIL 빌드: 멀티스레딩 효과 없음
Free-threaded: 코어 수에 비례하는 속도 향상
I/O 바운드 작업:
GIL 빌드: 멀티스레딩 효과 있음 (GIL이 I/O 중 해제)
Free-threaded: 동일한 효과 + 약간의 추가 개선
혼합 작업:
GIL 빌드: I/O 부분만 병렬화
Free-threaded: CPU + I/O 모두 병렬화Free-threaded Python의 가장 큰 도전은 생태계 호환성입니다.
GIL에 의존하여 작성된 C 확장은 free-threaded 모드에서 안전하지 않을 수 있습니다. 주요 라이브러리의 지원 현황은 다음과 같습니다.
완전 지원:
- NumPy 2.1+
- Cython 3.1+
- pydantic 2.x
부분 지원 / 진행 중:
- pandas
- scikit-learn
- matplotlib
미지원:
- 일부 소규모 C 확장 라이브러리import importlib.metadata
def check_free_threading_support(package_name: str) -> None:
"""패키지의 free-threading 지원 여부 확인"""
try:
meta = importlib.metadata.metadata(package_name)
classifiers = meta.get_all("Classifier") or []
for c in classifiers:
if "Free-Threading" in c or "free-threading" in c:
print(package_name + ": free-threading supported")
return
print(package_name + ": no free-threading classifier found")
except importlib.metadata.PackageNotFoundError:
print(package_name + ": not installed")Free-threaded 모드에서 올바른 코드를 작성하려면, 스레드 안전성에 대한 이해가 필요합니다.
import threading
# 안전하지 않음: 공유 리스트에 동시 접근
shared_list = []
def unsafe_append(items: list[int]) -> None:
for item in items:
shared_list.append(item) # 경쟁 조건 가능
# 안전함: Lock 사용
lock = threading.Lock()
def safe_append(items: list[int]) -> None:
for item in items:
with lock:
shared_list.append(item)import threading
# Lock: 기본적인 상호 배제
lock = threading.Lock()
# RLock: 재진입 가능 Lock (같은 스레드가 여러 번 획득 가능)
rlock = threading.RLock()
# Semaphore: 동시 접근 수 제한
semaphore = threading.Semaphore(value=3) # 최대 3개 스레드 동시 접근
# Event: 스레드 간 신호 전달
event = threading.Event()
# Barrier: 여러 스레드가 동시에 특정 지점에 도달할 때까지 대기
barrier = threading.Barrier(parties=4)free-threaded 모드에서도 GIL 시절의 threading 모듈과 동기화 프리미티브를 동일하게 사용합니다. 차이점은 GIL이 암묵적으로 제공하던 보호가 사라지므로, 공유 상태에 대한 명시적 동기화가 더 중요해진다는 것입니다.
기존에 GIL을 우회하기 위해 사용하던 multiprocessing과 비교합니다.
Free-threaded (threading):
장점:
- 메모리 공유 (프로세스 간 직렬화 불필요)
- 프로세스 생성 오버헤드 없음
- 공유 데이터 구조에 대한 빠른 접근
단점:
- 스레드 안전성을 직접 관리해야 함
- 실험적 기능 (Python 3.13)
multiprocessing:
장점:
- 완전한 격리 (한 프로세스 충돌이 다른 프로세스에 영향 없음)
- 오래되고 안정적인 API
단점:
- 프로세스 간 데이터 전송 비용 (직렬화/역직렬화)
- 높은 메모리 사용량 (각 프로세스가 독립적 메모리 공간)
- 프로세스 생성 오버헤드Free-threaded Python은 장기적인 프로젝트입니다.
Python 3.13 (2024) - 실험적 도입, 별도 빌드
Python 3.14 (2025) - 안정화, 더 많은 라이브러리 지원
Python 3.15 (2026) - 기본 빌드에 포함 가능성 검토
Python 3.16+ (2027~) - GIL 없는 빌드가 기본이 될 가능성Python Steering Council은 free-threaded 모드가 충분히 안정화되고, 주요 생태계가 호환성을 확보하면 기본으로 전환하겠다는 입장입니다.
Free-threaded Python은 30년간 Python의 근본적 제약이었던 GIL을 제거하는 역사적 변화입니다.
8장에서는 Python 3.13의 또 다른 실험적 기능인 JIT 컴파일러를 다룹니다. copy-and-patch 컴파일 기법의 원리, JIT 빌드 방법, 그리고 현재 성능과 미래 가능성을 분석합니다.
이 글이 도움이 되셨나요?
Python 3.13에 도입된 실험적 JIT 컴파일러를 분석합니다. copy-and-patch 기법의 원리, Tier 2 최적화 파이프라인, 빌드와 활성화 방법, 성능 벤치마크를 다룹니다.
Python 3.12의 성능 향상 원리를 분석합니다. 특수화 적응 인터프리터, 컴프리헨션 인라인화, immortal objects, asyncio 최적화 등 CPython 내부를 다룹니다.
Rust로 작성된 차세대 Python 패키지 매니저 uv를 다룹니다. 설치, 프로젝트 관리, 가상 환경, Python 버전 관리, 스크립트 실행까지 실전 워크플로우를 안내합니다.