GIL을 끄면 모든 스레드가 빨라질까: Python 3.14 free-threaded 빌드의 세 경계

"Python에서 GIL을 없앴다"는 문장만 보면 이제 threading으로 CPU 코어를 모두 쓰면 될 것 같다. Python 3.14의 free-threaded CPython은 실제로 여러 스레드가 서로 다른 코어에서 Python 코드를 병렬 실행할 수 있다. 다만 설치만 바꾸면 기존 코드가 자동으로 빨라진다는 뜻은 아니다.

공식 문서를 따라가면 확인할 대상이 세 층으로 나뉜다. free-threading을 지원하는 빌드인지, 현재 프로세스에서 GIL이 꺼져 있는지, 불러온 C 확장이 그 상태를 유지하는지다. 여기에 공유 상태의 동기화와 단일 스레드 비용까지 더해야 운영 판단이 가능해진다.

Python 3.14는 기본 전환이 아니라 지원 단계다

PEP 779는 free-threaded CPython의 전환을 세 단계로 설명한다. Python 3.13의 1단계는 실험적 빌드 제공, 3.14가 목표로 삼은 2단계는 공식 지원이지만 선택 사항인 빌드다. GIL 없는 빌드를 기본값으로 만드는 3단계는 별도 결정으로 남겨 뒀다.

이 구분은 배포 방식에 직접 영향을 준다. 일반 CPython을 설치한 뒤 설정 하나로 구현 전체가 바뀌는 구조가 아니다. 먼저 free-threaded 빌드를 선택해야 한다. python -VVsys.version에는 free-threading build가 표시되고, 빌드 설정은 sysconfig.get_config_var("Py_GIL_DISABLED")로 확인할 수 있다.

하지만 빌드가 지원한다고 현재 프로세스의 GIL이 꺼졌다고 단정할 수는 없다. PYTHON_GIL이나 -X gil 옵션으로 다시 켤 수 있고, 실제 런타임 상태는 sys._is_gil_enabled()로 봐야 한다.

free-threaded 빌드 확인에서 런타임 GIL 상태와 C 확장 호환성 확인으로 이어지는 세 단계 흐름

그림 1. 빌드가 free-threading을 지원해도 런타임 옵션이나 지원 표시가 없는 C 확장 때문에 GIL이 켜질 수 있다. 출처: docs.python.org/3/howto/free-threading-python.html.

C 확장 하나가 GIL을 다시 켤 수 있다

가장 쉽게 놓치는 부분은 의존성이다. Python 3.14 free-threading HOWTO는 free-threading 지원을 명시하지 않은 C API 확장 모듈을 import하면 경고를 내고 GIL을 자동으로 활성화할 수 있다고 적는다. 애플리케이션 코드가 순수 Python이어도 NumPy, 데이터베이스 드라이버, 압축 라이브러리처럼 네이티브 확장을 거치면 결과가 달라질 수 있다.

확장 모듈 작성자는 multi-phase initialization에서 Py_mod_gil 슬롯을 Py_MOD_GIL_NOT_USED로 표시하거나, single-phase initialization이면 PyUnstable_Module_SetGIL()을 호출해 지원 여부를 알린다. 표시만 추가해서 끝나는 것은 아니다. 확장 내부의 전역 캐시, 직접 struct 필드 접근, 잠금 없는 accessor 매크로, borrowed reference가 동시 수정과 만나는지 검토해야 한다.

공식 C 확장 HOWTOPyList_GetItem()처럼 borrowed reference를 돌려주는 API가 컨테이너 동시 수정과 함께 쓰이면 안전하지 않다고 설명한다. 가능한 곳에서는 PyList_GetItemRef()PyDict_GetItemRef() 같은 strong reference API로 바꾸고, PyDict_Next() 순회처럼 내부 잠금이 없는 경로는 critical section으로 보호해야 한다.

내장 컨테이너의 잠금은 비즈니스 원자성이 아니다

free-threaded 빌드의 dict, list, set은 동시 수정에 대비한 내부 잠금을 사용한다. 그렇다고 "여러 스레드가 같은 dict를 마음대로 바꿔도 안전하다"는 언어 계약이 생긴 것은 아니다. 공식 문서는 이를 현재 구현의 설명으로만 보라고 선을 긋고, 가능하면 threading.Lock 같은 동기화 수단을 쓰라고 권한다.

예를 들어 if key not in cache:를 확인한 뒤 값을 계산하고 cache[key] = value를 실행하는 흐름은 여러 연산으로 이루어진다. 각 dict 호출이 내부적으로 보호되더라도 두 스레드가 동시에 조건을 통과할 수 있다. 중복 계산이나 외부 부작용을 막아야 한다면 애플리케이션 수준의 잠금이 필요하다.

같은 iterator를 여러 스레드에서 공유하는 것도 피해야 한다. Python 3.14 문서는 항목이 중복되거나 누락될 수 있다고 명시한다. frame.f_locals를 다른 스레드가 실행 중인 프레임에서 읽는 행위는 인터프리터 충돌까지 일으킬 수 있다.

Python 코드의 명시적 잠금과 C 확장의 strong reference 및 critical section 검토를 운영 검증과 연결한 안전 경계

그림 2. 내장 컨테이너의 내부 잠금과 C API의 지원 표시만으로 애플리케이션의 여러 단계 연산이 원자적이 되지는 않는다. 출처: docs.python.org/3/howto/free-threading-extensions.html.

단일 스레드와 메모리 비용을 함께 재야 한다

병렬 실행 가능성에는 대가가 있다. Python 3.14 HOWTO가 인용한 pyperformance 평균에서 free-threaded 빌드의 단일 스레드 오버헤드는 macOS aarch64 약 1%부터 x86-64 Linux 약 8%까지다. 이 숫자는 특정 서비스의 예측치가 아니다. 하드웨어와 워크로드에 따라 달라지므로 대조 실험의 출발점으로만 써야 한다.

메모리도 늘 수 있다. free-threaded 빌드는 일부 객체를 불멸화해 참조 횟수 경쟁을 피하고, 잠금 없는 자료구조의 안전한 메모리 회수를 위해 해제를 미룰 수 있다. AMD64에서 None 같은 non-GC 객체의 헤더가 기본 빌드의 16바이트에서 32바이트가 되는 예도 공식 문서에 나온다. 작은 객체를 대량으로 만드는 워크로드라면 처리량만 보고 전환하기 어렵다.

free-threaded Python의 단일 스레드 오버헤드 약 1에서 8퍼센트, 메모리 증가 가능성, 공유 iterator 비안전성을 비교한 세 카드

그림 3. 병렬 이득은 단일 스레드와 메모리 비용 및 공유 객체의 안전성까지 같은 워크로드로 비교해야 한다. 출처: docs.python.org/3/howto/free-threading-python.html.

좋은 후보와 나쁜 후보를 먼저 나눈다

free-threading의 이득이 기대되는 쪽은 CPU를 오래 쓰는 독립 작업이 많고, 스레드 사이에 공유 쓰기가 적은 프로그램이다. 여러 이미지나 문서를 서로 독립적으로 변환하는 배치 작업은 비교적 설명하기 쉽다. 반대로 하나의 전역 캐시를 자주 갱신하거나, 네이티브 확장이 아직 GIL을 요구하거나, 대부분의 시간을 네트워크 대기에 쓰는 서비스는 이득이 작거나 측정이 복잡할 수 있다.

운영 검증은 다음 순서가 현실적이다.

  1. 같은 Python 3.14 free-threaded 빌드에서 GIL ON과 OFF를 모두 실행한다.
  2. 시작 직후 빌드 지원 값과 sys._is_gil_enabled() 결과를 로그에 남긴다.
  3. production 의존성을 모두 import한 뒤 GIL 상태를 다시 확인한다.
  4. 같은 입력으로 처리량, p95/p99 지연, CPU 사용률, RSS를 함께 비교한다.
  5. 공유 컨테이너와 확장 전역 상태를 목록으로 만들고 명시적 잠금 경계를 검토한다.

일반 CPython만 대조군으로 두면 빌드 차이와 GIL 상태가 섞인다. 같은 free-threaded 바이너리에서 GIL을 다시 켠 실행도 함께 두면 두 효과를 더 잘 나눌 수 있다.

결론

Python 3.14의 free-threaded 빌드는 "스레드를 쓰면 무조건 빨라진다"는 기능이 아니다. 공식 지원 단계에 들어간 별도 빌드이며, 런타임 옵션과 C 확장 때문에 GIL이 다시 켜질 수 있다. 내장 컨테이너의 내부 잠금도 여러 단계의 상태 변경을 자동으로 원자화하지 않는다.

따라서 전환의 첫 질문은 "GIL을 없앨까"가 아니라 "우리의 CPU 병렬 구간이 어디이고, 어떤 공유 상태와 확장이 그 경계를 가로지르는가"다. 그 지도를 그린 뒤 GIL ON/OFF를 같은 입력으로 비교해야 free-threading이 실제 이득인지 판단할 수 있다.

참고 자료

댓글

이 블로그의 인기 게시물

체크아웃은 분리됐는데 Git 상태는 공유된다: Codex 0.154 worktree의 세 경계

잠근 작업이 커밋 뒤 다시 나온다: PostgreSQL 에이전트 큐의 소유권 설계

코딩 에이전트는 API 문서보다 AGENTS.md를 먼저 읽었다