파이썬의 GIL
파이썬을 어느 정도 사용해 본 사람이라면 “GIL(Global Interpreter Lock)”이라는 용어를 한 번쯤은 들어 봄.
GIL은 파이썬 개발 및 운영 환경에서 동시성(Concurrency) 이슈를 다룰 때 매우 중요한 요소로 자주 언급됨.
GIL이란 무엇인지
1. 정의
GIL은 CPython(표준 파이썬 구현체)에서 동시에 여러 스레드가 바이트코드를 실행하지 못하도록 하는 '글로벌(전역) 락'임.
즉, 인터프리터 자체에서 단 한 스레드만이 바이트코드(파이썬 코드가 바이트코드로 컴파일된 결과)를 실행하도록 보장하는 일종의 상호 배제(Mutual Exclusion) 장치임.
2. 의미
여러 CPU 코어가 있는 멀티코어 환경에서, 동일한 프로세스 안에서 여러 파이썬 스레드를 실행하더라도 동시에 CPU 연산을 수행하는 것은 불가능하게 만듬.
물론 I/O는 스레드마다 병렬적으로 진행될 수 있음.
GIL이 존재하는 이유
1. CPython 구현의 단순화
파이썬 인터프리터(CPython)는 C 언어로 작성되어 있으며, 내부적으로 객체 관리(메모리 할당, 참조 카운팅 등)를 수행함.
GIL은 이런 레퍼런스 카운팅을 포함한 객체 관리 로직을 간단하면서도 안전하게 구현하기 위해 도입된 것임.
레퍼런스 카운팅이 동시에 여러 스레드에서 수행될 때 원자성이 깨지면 참조 횟수가 잘못 갱신되어 메모리 누수나 오작동이 일어날 수 있음.
GIL을 두면 이러한 객체 관리, 특히 레퍼런스 카운팅 관련 로직에서 락 충돌이나 데드락을 피하기가 비교적 쉬워짐.
2. 역사적/레거시 이슈
GIL은 파이썬이 초기 설계되었을 때부터 존재해 왔음.
당시 하드웨어 환경이 지금처럼 멀티코어가 보편화되지 않았고, 파이썬 해석기의 구현 단순성이 더 중요한 고려사항이었음.
3. 코드 유지보수 측면
GIL이 없다면, 파이썬 해석기 내부에서 발생할 수 있는 많은 경쟁 상태(Race Condition)와 데드락, 복잡한 락 계층 관리 등의 문제가 잔뜩 발생함.
CPython의 코드 베이스가 매우 커졌고 오랜 역사를 가지고 있기 때문에, 기존 구조를 근본적으로 뒤엎기는 쉽지 않음.
GIL의 동작 방식 : CPython에서의 스레드 스위칭 메커니즘
1. 바이트코드 레벨에서의 스위치
파이썬 인터프리터는 일정 횟수의 바이트코드를 실행하면 스위치를 시도함(Python 3.9 기준, 일정 횟수의 바이트코드를 실행한 다음 [Python 3.10에서 이 설정이 변경될 수 있음]).
예를 들어, 기존에는 sys.setswitchinterval(), sys.setrecursionlimit() 등에 의해서도 영향을 받을 수 있었음.
이때 GIL을 소유한 스레드가 일정 작업을 한 뒤, 다른 스레드에게 GIL을 넘겨주는 방식으로 스케줄링함.
2. 실행 흐름
스레드 A가 GIL을 획득하고 바이트코드를 일정 횟수 실행.
스레드 A가 GIL을 해제하거나, 스위치 조건이 되면 인터프리터는 GIL을 다른 스레드 B에게 넘길 수 있음.
스레드 B가 GIL을 획득하고 바이트코드를 실행.
GIL의 동작 방식 : I/O 바운드 작업과 GIL
1. I/O 바운드 작업의 병렬성
파이썬 스레드는 대기/차단 상태(I/O 대기, Sleep 등)에선 GIL을 해제할 수 있음.
그 결과, 다른 스레드가 GIL을 얻어 CPU를 사용할 수 있음.
예를 들어, 네트워크 요청, 파일 읽기/쓰기 등은 GIL을 잠시 해제하므로, 여러 스레드가 동시에 I/O 작업을 처리하는 것처럼 보이게 할 수 있음.
2. CPU 바운드 작업에서의 병목
숫자 연산처럼 CPU를 많이 사용하는 연산(NumPy 행렬 연산, 대규모 반복문 계산 등)은 스레드가 GIL을 오래 소유하게 되므로 동시에 실행하지 못함.
GIL로 인한 영향 : 장점
1. 구현 단순화 및 안정성
GIL 덕분에 CPython 내부에서의 메모리 관리, 객체 참조 등은 복잡한 락 계층을 구현하지 않아도 됨.
전반적인 구현 유지보수가 쉬워서 파이썬의 다양한 버전과 플랫폼 이식성도 확보하기 수월함.
2. C 확장 모듈과의 연동 용이
CPython에서 C로 작성된 확장 모듈은 GIL을 이용해 쉽게 파이썬 객체를 다룰 수 있음.
GIL을 가진 상태에서 파이썬 객체에 접근하면 안전하게 참조 카운팅을 조작할 수 있음.
GIL로 인한 영향 : 단점
1. 멀티코어 CPU 활용의 제약
CPU 바운드 작업에서는 멀티코어를 제대로 활용하기가 어려움.
예를 들어, 4코어 머신에서 멀티스레딩으로 계산 작업을 나누어도 GIL 때문에 실제로는 CPU 한 코어만 활용됨.
시간 분할로 나눠서 실행함.
2. 잠재적 성능 저하
스레드를 많이 생성해도, CPU 연산이 많다면 GIL이 병목이 되어 스레드 수만큼 성능이 선형 증가하지 않음.
불필요한 context switching과 락 획득 경쟁이 늘어나면 오히려 오버헤드가 증가할 수 있음.
GIL 문제를 극복하는 방법 : 멀티프로세싱
1. multiprocessing 모듈
파이썬 표준 라이브러리에서 제공하는 멀티프로세싱 기능을 사용하면, 프로세스 간에 GIL이 각각 존재하므로 여러 프로세스가 멀티코어를 동시에 활용할 수 있음.
각 프로세스마다 독립된 메모리 공간이 있고, 데이터를 교환할 때는 직렬화(pickle) 과정을 거쳐야 하므로 IPC 비용이 발생하지만, CPU 바운드 작업을 병렬 처리하기에는 효과적임.
2. 프로세스 풀(Process Pool)
multiprocessing.Pool이나 concurrent.futures.ProcessPoolExecutor 등을 사용해 쉽게 프로세스 풀을 만들어 작업을 분산할 수 있음.
GIL 문제를 극복하는 방법 : C/C++ 확장 모듈 사용
1. GIL 해제 가능
C나 C++로 구현된 확장 모듈에서 Py_BEGIN_ALLOW_THREADS, Py_END_ALLOW_THREADS 매크로를 이용하면 GIL을 일시적으로 해제하고 순수 C/C++ 코드에서 병렬 연산을 할 수 있음.
NumPy, SciPy, TensorFlow, PyTorch 등 많은 라이브러리가 내부적으로 이런 메커니즘을 통해 병렬 수행을 최적화함.
2. Cython
Cython을 이용해 파이썬 코드를 C 레벨로 컴파일하고, 필요한 부분에서 GIL을 해제할 수 있음(with nogil: 구문 등).
CPU 바운드가 큰 부분을 Cython으로 짜서 GIL을 해제하고 동시 실행이 가능하도록 만들 수 있음.
GIL 문제를 극복하는 방법 : 다른 파이썬 구현체
1. PyPy
JIT(Just-In-Time) 컴파일러를 통해 CPython보다 빠른 실행 속도를 보여주는 경우가 있지만, 여전히 GIL이 존재함.
2. Jython
자바로 구현된 파이썬. 자바 VM 위에서 동작하기 때문에 GIL이 없음.
그러나 CPython과 호환되지 않는 부분이 있고, C 확장 모듈을 직접 사용할 수 없음.
3. IronPython
.NET 기반의 파이썬 구현. 역시 GIL이 없지만, 마찬가지로 CPython과 완벽히 호환되지 않음.
GIL 개선 시도와 한계 : 예전 시도들
1. Greg Stein의 GIL 제거 시도 (Python 1.5 시대)
CPython에서 GIL을 제거하고 세밀한 락을 도입하여 멀티스레딩 성능을 높이려 했으나, 오히려 전체적으로 성능이 떨어지고 구현 복잡도만 증가함.
이후 “GIL을 유지하는 것이 낫다”는 결론에 도달.
2. Efficient Concurrency Model 연구
GIL 문제를 해결하기 위해, 파이썬 커뮤니티에서는 여러 가지 개선안(예: Software Transactional Memory, STM)을 논의했지만, CPython 메인라인에 직접 반영된 사례는 많지 않음.
GIL 개선 시도와 한계 : 최근 동향
1. 다층적인 접근
CPython 내부 구조를 획기적으로 바꾸는 것은 큰 도전이며, 기존 라이브러리 및 확장 모듈 호환성, 안정성 문제가 컸음.
대신, CPython은 부분적인 최적화를 통해서 스레드 스위칭 오버헤드를 줄이거나, I/O 모델을 개선하는 식으로 접근하고 있음.
2. Sub-Interpreters와 메모리 분리
Python 3.8 이후, 여러 서브 인터프리터(sub-interpreter)를 동시에 돌릴 수 있는 multiprocessing 유사 방식이 연구되고 있음.
각 서브 인터프리터별로 GIL이 독립적으로 생성되도록 설계가 진행 중이며, 성공적으로 도입되면 멀티코어 활용이 좀 더 쉬워질 수 있음.
요약
1. GIL의 핵심
CPython에서는 전역 인터프리터 락이 있어서 한 시점에 오직 하나의 스레드만 바이트코드를 실행함.
이는 레퍼런스 카운팅 기반의 객체 관리 로직을 간단하고 안전하게 보장하기 위함임.
2. 멀티코어 활용 문제
CPU 바운드 작업의 경우 GIL이 병목이 되어 멀티스레드 이점이 제한적임.
따라서 멀티프로세싱, C 확장 모듈(NumPy, Cython 등)을 이용하거나 다른 파이썬 구현체(Jython, IronPython)를 사용하는 등 우회책을 마련해야 함.
3. 미래
CPython의 근본적 구조 변경은 호환성과 복잡성 문제로 쉽지 않음.
그럼에도, 특정 확장 모듈의 GIL 해제, sub-interpreter 모델 도입 등 여러 방안으로 점진적인 개선이 이루어지고 있음.
결론적으로, 파이썬 GIL은 초기 CPython 구현의 설계 구조와 역사가 깊이 얽힌 중요한 요소임.
파이썬 개발자로서 GIL을 완벽히 제거할 수는 없지만, 그 동작 방식을 깊이 이해하고, 프로그램 특성에 따라 적절한 우회책(멀티프로세싱, C 확장, Cython, async/await, sub-interpreter 등)을 활용한다면, 멀티코어 시대에도 충분히 확장성과 성능을 확보할 수 있음.
'Programming Language > Python' 카테고리의 다른 글
[Python] 파이썬의 런타임 (0) | 2025.01.11 |
---|---|
[Python] PYTHONPATH 활용 (0) | 2025.01.11 |
[Python] CPU 아키텍처와 파이썬 라이브러리의 영향 (0) | 2025.01.11 |
[Python] 파이썬 환경을 분리해야 하는 이유 (0) | 2025.01.11 |
[Python] Miniconda 주요 특징 (0) | 2025.01.11 |