GIL(Global Interpreter Lock)은 CPython 인터프리터에서 한 번에 하나의 스레드만 Python 바이트코드를 실행할 수 있도록 강제하는 뮤텍스이다.
레퍼런스 카운팅과 GIL
CPython은 객체의 메모리 관리를 레퍼런스 카운팅으로 한다. 모든 객체에 참조 횟수(ob_refcnt)가 있고, 이 값이 0이 되면 메모리를 해제한다.
// CPython 내부의 참조 카운트 조작Py_INCREF(op); // ob_refcnt++Py_DECREF(op); // ob_refcnt--, 0이면 해제여러 스레드가 동시에 이 카운터를 수정하면 race condition이 발생한다. 예를 들어 두 스레드가 동시에 Py_DECREF를 호출하면, 카운터가 1인 객체를 두 번 해제하거나 아예 해제하지 못하는 상황이 생길 수 있다.
이를 해결하는 방법은 두 가지이다.
- 객체별 fine-grained 락: 모든 객체마다 개별 락을 건다. 오버헤드가 크고 데드락 위험이 있다. 실제로 1999년 Greg Stein이 CPython에서 GIL을 제거하고 fine-grained 락을 적용하는 패치를 만들었지만, 싱글스레드 성능이 40% 이상 떨어져서 채택되지 않았다.
- 인터프리터 전체 락(GIL): 인터프리터에 락을 하나만 둔다. 단순하고 싱글스레드 성능 저하가 없다.
CPython은 후자를 택했다.
GIL의 동작
Python 스레드는 일정량의 바이트코드를 실행한 뒤 GIL을 해제하고 다른 스레드에 실행 기회를 넘긴다. Python 3.2 이전에는 sys.setcheckinterval()로 바이트코드 명령어 수 기준으로 전환했고, 3.2부터는 시간 기반(기본 5ms)으로 변경되었다.
import syssys.getswitchinterval() # 기본값: 0.005 (5ms)sys.setswitchinterval(0.001) # 1ms로 변경I/O 작업(네트워크, 파일, sleep 등)을 할 때는 GIL을 해제한다. 따라서 I/O-bound 멀티스레딩은 GIL의 영향을 거의 받지 않는다. C 확장 모듈도 Py_BEGIN_ALLOW_THREADS 매크로로 GIL을 명시적으로 해제하고 작업할 수 있어서, numpy 같은 라이브러리는 실제로 멀티코어를 활용한다.
// C 확장에서 GIL 해제Py_BEGIN_ALLOW_THREADS// GIL 없이 실행되는 CPU-intensive 코드heavy_computation();Py_END_ALLOW_THREADSCPU-bound에서의 영향
GIL은 뮤텍스이므로, Python 바이트코드를 실행하려면 반드시 GIL을 획득해야 한다. GIL은 하나뿐이기 때문에 스레드가 아무리 많아도 GIL을 잡고 있는 스레드 하나만 Python 코드를 실행할 수 있고, 나머지 스레드는 전부 GIL이 풀릴 때까지 대기한다. 결국 CPU-bound 작업에서는 스레드들이 번갈아가며 순차 실행하는 것과 같아서, 코어가 여러 개 있어도 실제 병렬 실행이 이루어지지 않는다.
import threading, time
def count(): n = 0 for _ in range(50_000_000): n += 1
# 싱글스레드start = time.time()count()count()print(f"sequential: {time.time() - start:.2f}s")
# 멀티스레드 - GIL 때문에 더 빨라지지 않음start = time.time()t1 = threading.Thread(target=count)t2 = threading.Thread(target=count)t1.start(); t2.start()t1.join(); t2.join()print(f"threaded: {time.time() - start:.2f}s")오히려 스레드 간 GIL 경합과 컨텍스트 스위칭 비용 때문에 멀티스레드가 더 느릴 수 있다.
우회 방법
multiprocessing
프로세스를 분리하면 각각 독립된 인터프리터와 GIL을 가진다.
from multiprocessing import Pool
def heavy(n): return sum(range(n))
with Pool(4) as p: results = p.map(heavy, [10**7] * 4)프로세스 간 데이터 교환은 pickle 직렬화를 거치므로 IPC 오버헤드가 있다. 작업 단위가 충분히 클 때 효과적이다.
C 확장에서 GIL 해제
연산 집중 부분을 C, Cython, Rust 등으로 작성하고 GIL을 해제한다. numpy, scikit-learn 같은 라이브러리가 이 방식으로 멀티코어를 활용한다.
asyncio
I/O-bound 작업은 코루틴으로 동시성을 확보한다. GIL과 직접적인 관계는 없지만, I/O 대기 시간을 효율적으로 활용하는 대안이다.
다른 Python 구현체의 GIL
- PyPy: GIL이 있다. STM(Software Transactional Memory) 기반 제거 시도가 있었지만 실용화되지 않았다.
- Jython: GIL이 없다. JVM의 스레딩 모델을 그대로 사용한다.
- IronPython: GIL이 없다. .NET CLR 위에서 동작한다.
- RustPython: GIL이 있다.
Free-threaded CPython (PEP 703)
CPython 3.13부터 GIL을 제거한 실험적 빌드가 도입되었다.
# free-threaded 빌드 (3.13+)./configure --disable-gilmakeGIL 없이 레퍼런스 카운팅의 안전성을 보장하기 위해 biased reference counting, deferred reference counting 등의 기법을 사용한다. 싱글스레드 성능 저하를 최소화하면서 멀티스레드 병렬 실행을 가능하게 하는 것이 목표이다.
다만 기존 C 확장 모듈들이 GIL 존재를 전제로 작성되어 있어서, 생태계 전체가 thread-safe하게 업데이트되기까지는 시간이 걸린다. CPython 3.13에서는 실험적 기능이며, 점진적으로 안정화될 예정이다.
참고