파이썬/GIL 제거 동향

[파이썬] GIL: 무엇이고, 왜 필요하고, 왜 못 없애나

Hun's blog 2026. 6. 1. 10:21

 

파이썬을 처음 배울 때  파이썬은 멀티 스레드를 사용해도 병렬처리가 되지 않는다고 배웠다. 이것을 GIL이라고 배웠고, 단순히 인터프리터가 멀티스레드 환경에서 한 개의 스레드만 사용하도록 강제했고 우회할 대안으로는 multiprocessing 라이브러리를 사용해서 멀티 프로세스로 접근하라고 외웠다.

 

실무에서 파이썬을 다루면서 실제 내부 구조를 깊게 파악하고 싶다는 호기심이 생겼다. 특히 최근에 화두가 되고 있는 파이썬의 GIL 제거 시도부터 공부하면서 파이썬 내부를 분석해보고자 한다.

 

이번 포스트에서는 GIL의 정의와 역사, 그리고 초기 제거 시도에 대해서 다루고자 한다.


GIL(Global Interpreter Lock) 이란?

인터프리터 프로세스가 멀티 스레드 환경에서 하나의 스레드에게만 Lock을 부여하고 나머지는 대기시키는 방식

GIL이 필요한 이유

  • 파이썬 인터프리터인 CPython은 참조 횟수(Reference Count)를 통해 메모리를 정리
  • 특정 객체(문자열, 튜플, 리스트 등)가 변수에 할당되거나, 다른 함수 혹은 메소드의 인자로 전달되면 참조 + 1, 해제되면 -1 되는 참조 횟수 증감 연산을 하고 있음
  • 참조 횟수가 0이 될 경우, 객체를 메모리에서 제거(free)
  • 하지만 참조 횟수는 멀티 스레드 환경에서 동시 접근에 의해 경쟁 상태(Race Condition)에 빠질 수 있으며 각 스레드 참조 횟수의 정합성을 훼손할 수 있음
  • 그래서 멀티 스레드 상황에서도 인터프리터당 하나의 스레드가 동작하도록 Lock을 부여하여 동시 접근을 방지함
  • 이 외에도, 멀티스레드 환경에서 동기 접근에 안전한 Thread-Safe 한 조건을 요구하는 C Extension을 만족시키기 위함

GIL을 도입할 수 밖에 없는 현실적인 이유

  • 파이썬이 첫 개발 되던 시절에는 단일 코어 중심의 하드웨어었고, 운영 체제 차원에서도 스레드라는 개념을 지원하지 않거나, 지원하더라도 구현 방식이 달랐음
  • 파이썬의 주요 기능들은 C 언어로 구현되거나 C 라이브러리를 가져와서 사용
  • C Extension은 Thread-Safe 한 메모리 관리를 요구했고 GIL이 가장 적합하여 적용
  • 시간이 지나 멀티 스레드 기술이 성숙해지고 하드웨어 사양이 충분해졌을 때, GIL로 인한 문제가 수면 위로 드러남
  • 하지만 오랜 기간 동안 쌓인 레거시들의 하위 호환성을 고려했을 때 GIL을 제거하고 다른 방안을 도입하기 어려운 상황이 됨

GIL이 파이썬에게 의미있는 도입일 수도 있는 이유

  • Python이 개발 되던 당시 주류 언어는 C언어
  • Python이 GIL을 통해 C Extension을 사용할 수 있게 되고, 기존 C를 사용하던 개발자들이 Python으로 쉽게 넘어올 수 있도록 만듬
  • 결과적으로 Python 커뮤니티는 성장할 수 있었고, 현재 AI, 데이터 분야에 대표적인 언어로 정착

GIL을 제거하려는 노력

GIL을 제거하려는 노력은 꾸준히 있어왔다. 크게는 1996년 Greg Stein의 free-threading 패치부터 PEP 703, PEP 779, PEP 803 같이 현재에도 시도를 하고 있지만 완전한 제거는 이루지 못했고, 선택적인 비활성화 수준의 기능을 제공하고 있다.

 

이번 글에서는 Greg Stein의 free-threading(1996)와 Larry Hasting의 Gilectomy(2016)을 먼저 다루고, 이후 포스팅에서 실제로 파이썬 운영위원회(Steering Council)에서 승인받은 PEP703와 이후 최신 현황에 대해서 다룰 예정이다.


Greg Stein의 free-threading

1996년 Greg Stein 은 Python 1.4 버전을 기반으로 GIL을 제거하고 멀티스레딩을 구현하기 위한 최초의 'free-threading' 패치를 시도했다. 그는 인터프리터 전체를 묶고 있던 거대한 Lock을 없애는 대신 다음과 같은 구조적 변화를 제안했다.

 

핵심 아키텍처

  1. 스레드 상태 격리
    • 인터프리터의 전역 상태를 스레드의 독립된 상태로 편입
    • 스레드 상태를 연결 리스트로 연결
    • 스레드 상태를 조회 시, 리스트를 순회하며 스레드 ID를 탐색하고 리스트 맨 앞으로 이동시킨 후에 반환
  2. 참조 횟수 연산의 세분화된 자금 (Fine-Grained Locking)
    • 참조 횟수 증감연산을 할 때 Lock을 부여
    • 멀티 스레드 환경에서 동시 작업이 가능하지만 동시에 증감연산은 금지
    • A 스레드가 I 객체의 참조 횟수를 증감연산 할 때 B 스레드가 J 객체의 참조 횟수를 증감 불가
  3. 가변(mutable) 객체들의 잠금
    • 참조 횟수 연산에서는 전역 자물쇠를 사용하지만, 리스트나 딕셔너리 같은 가변 객체들은 개별적으로 Lock 부여
    • 중첩 리스트에서는 Lock 해제를 재귀적으로 수행

 

결론

결과는 처참했다. 단일 스레드 작업은 1.9초에서 12.7초로, 멀티스레드 작업은 2.5초에서 18.5초로 늘어나며 기존 대비 약 6~7배의 성능 저하(속도 기준 85% 감소)를 기록했다 [1]. 성능이 급감한 결정적인 이유는 참조 횟수를 증감할 때마다 스레드별로 뮤텍스(Mutex) Lock을 매번 수행하면서 발생한 오버헤드 때문이었다. 비록 패치는 실패로 끝났지만, 이 시도를 계기로 파이썬 커뮤니티에는 'GIL을 제거하더라도 싱글 스레드의 성능을 희생시켜서는 안 된다'라는 철칙이 깊게 새겨지게 된다."


Larry Hasting의 Gilectomy

2016년 Larry Hasting은 Pycon에서 Gilectomy를 소개했다(Gilectomy는 GIL + ectomy (절제술)을 합친 합성어). 기존의 Greg Stein이 참조 횟수 연산을 할 때 모든 스레드가 접근 가능하되, 연산 시에 Lock을 부여하는 것과 달리, 한 스레드에 모든 참조 증감 연산의 책임을 위임하는 아이디어를 제안했다. 

 

핵심 아키텍처

Buffered reference counting

  • 하나의 스레드에 참조 횟수 증감 연산의 책임을 위임
  • 다른 스레드들은 자신의 상태에 증감 연산 로그를 추가
  • 증감 연산 담당 스레드가 모든 스레드의 로그를 취합하여 참조 횟수 연산을 수행

특이사항

  • 처음에는 한 스레드에 증감 연산과 로그 상태 보관을 담당시켜, 다른 스레드들이 직접 로그를 작성시키려 함
  • 이 과정에서 스레드끼리 쓰기 경쟁이 발생하여, 각 스레드에 로그를 분산 시킴
  • 분산된 로그는 타임스탬프가 없어서 취합 시에 순서 문제가 발생
  • 특히, 감소 -> 증가 환경에서는 문제가 심각 (0 -> -1 -> 0) <- 이미 객체가 삭제됨
  • 그래서 모든 로그들 중 증가 연산을 먼저 수행하도록 수정
  • 이 방법은 참조 횟수의 실시간성을 보장하지 못한다는 단점이 존재

 

결론

성능은 기존 CPython에 근접했다고 주장했으나 이를 뒷받침할 벤치마크 근거는 없었다. 게다가 C API와의 호환성 문제로 귀도 반 로섬과 의견 대립을 겪으며 결국 채택되지 못했다. 하지만 이 제안은 CPython 내부의 병목 지점을 구체화하고, C API 구조 개편의 필요성을 대두시키는 계기가 되었다.


다음 글 예고

Greg Stein과 Larry Hasting의 시도는 비록 반려되었지만, 파이썬 내부의 병목 지점을 확인하고 C API 개편의 필요성을 공론화하는 중요한 밑거름이 되었다.

 

다음 글에서는 이들의 한계를 극복하고 마침내 파이썬 표준에 채택된 PEP 703과 최신 No-GIL 동향을 다룬다. 과연 어떤 기술적 돌파구를 통해 싱글 스레드의 성능 저하 없이 GIL을 선택적으로 비활성화할 수 있었는지, 그 핵심 아키텍처를 자세히 알아볼 예정이.