Python - GIL(Global Interpreter Lock, 전역 인터프리터 락)
1월 2일 Agent Developer 면접을 보러 가게 되었고, Python에 대해서 부족한 부분이 많기 때문에 공부를 하던 중 GIL에 대해 알게 되었습니다.
다른 언어와 비교했을 때 큰 차이가 나는 부분이고, Python을 깊이 이해하는 데 핵심이라고 생각해서 정리하게 되었습니다.
GIL(Global Interpreter Lock)이란?
GIL은 Python 인터프리터가 한 번에 하나의 Thread만 Python 바이트코드를 실행하도록 제한하는 뮤텍스(Mutex)입니다.
쉽게 말해, 멀티 스레딩을 해도 실제로는 동시에 하나의 Thread만 Python 코드를 실행할 수 있다는 뜻입니다.
NOTE
GIL은 CPython(가장 널리 사용되는 Python 구현체)에만 존재합니다.
Jython, IronPython, PyPy 등 다른 구현체는 GIL이 없거나 다른 방식으로 동작합니다.
처음 이 개념을 들었을 때, "왜 굳이 GIL이 필요하지? 오히려 느려질 것 같은데?"라는 생각을 했습니다.
먼저 GIL의 동작을 간단하게 확인해보겠습니다.

위 그림에서 볼 수 있듯이, GIL이 있으면 여러 Thread가 있어도 시간당 하나의 Thread만 실행됩니다.
겉으로 보기에는 비효율적으로 보일 수 있지만, 이것이 필요한 이유가 있습니다.
Python의 메모리 관리와 GIL의 필요성
GIL을 근본적으로 이해하기 위해서는 Python의 메모리 관리 방식을 알아야 합니다.
Python에서 모든 것은 객체다
Python에서는 정수, 문자열, 리스트 심지어 함수까지 모든 것이 객체(Object) 입니다.
그리고 Python은 이 객체들의 메모리를 Reference Counting(참조 카운팅) 방식으로 관리합니다.
import sys
a = [] # 리스트 객체 생성, refcount = 1
b = a # 같은 객체를 참조, refcount = 2
print(sys.getrefcount(a)) # 3 (getrefcount 호출 시 임시 참조 +1)
del b # 참조 해제, refcount = 2
del a # 참조 해제, refcount = 1 → 0이 되면 메모리 해제
Reference Count가 0이 되면 GC(Garbage Collector)가 해당 객체의 메모리를 해제합니다.
GIL이 없다면 어떤 문제가 생길까?
Reference Count는 단순한 정수 변수입니다. 만약 두 개의 Thread가 동시에 같은 객체의 Reference Count를 수정하면 어떻게 될까요?
Thread A: count 읽기 (현재 값: 2)
Thread B: count 읽기 (현재 값: 2)
Thread A: count + 1 = 3 저장
Thread B: count + 1 = 3 저장 ← 실제로는 4가 되어야 하지만 3이 됨!
이것이 바로 Race Condition(경쟁 상태) 입니다.
WARNING
Race Condition으로 인해 Reference Count가 잘못되면:
- Count가 실제보다 낮아지면 → 아직 사용 중인 객체가 해제되어 프로그램 크래시
- Count가 실제보다 높아지면 → 객체가 해제되지 않아 메모리 누수
Python의 메모리 관리는 Thread-safe하지 않습니다. 모든 객체에 개별 Lock을 걸면 성능 저하와 Deadlock 위험이 있기 때문에, Python은 하나의 전역 Lock(GIL) 으로 이 문제를 해결했습니다.
GIL과 성능: CPU-bound vs I/O-bound
GIL의 영향은 작업 유형에 따라 크게 달라집니다.
CPU-bound 작업
CPU-bound 작업은 계산 위주의 작업입니다. (예: 수학 연산, 이미지 처리, 데이터 분석)
import threading
import time
def cpu_intensive():
count = 0
for _ in range(10_000_000):
count += 1
return count
# 단일 스레드
start = time.time()
cpu_intensive()
cpu_intensive()
print(f"단일 스레드: {time.time() - start:.2f}초")
# 멀티 스레드
start = time.time()
t1 = threading.Thread(target=cpu_intensive)
t2 = threading.Thread(target=cpu_intensive)
t1.start(); t2.start()
t1.join(); t2.join()
print(f"멀티 스레드: {time.time() - start:.2f}초")
결과: 멀티 스레드가 단일 스레드보다 더 느리거나 비슷합니다.
GIL로 인해 직렬 실행되면서, 스레드 전환(Context Switching) 오버헤드까지 추가되기 때문입니다.
I/O-bound 작업
I/O-bound 작업은 외부 입출력을 기다리는 작업입니다. (예: 네트워크 요청, 파일 읽기/쓰기, DB 쿼리)
import threading
import time
import urllib.request
def fetch_url(url):
urllib.request.urlopen(url)
urls = ["https://google.com"] * 5
# 순차 실행
start = time.time()
for url in urls:
fetch_url(url)
print(f"순차 실행: {time.time() - start:.2f}초")
# 병렬 실행
start = time.time()
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for t in threads: t.start()
for t in threads: t.join()
print(f"병렬 실행: {time.time() - start:.2f}초")
결과: 멀티 스레드가 훨씬 빠릅니다!
TIP
I/O 대기 중에는 GIL이 자동으로 해제됩니다.
한 Thread가 네트워크 응답을 기다리는 동안, 다른 Thread가 GIL을 획득해서 작업할 수 있습니다.
GIL을 우회하는 방법
CPU-bound 작업에서도 병렬 처리가 필요하다면, 다음 방법들을 사용할 수 있습니다.
1. multiprocessing 모듈
각 프로세스는 독립적인 Python 인터프리터와 GIL을 가집니다.
from multiprocessing import Pool
import time
def cpu_intensive(n):
count = 0
for _ in range(10_000_000):
count += 1
return count
if __name__ == "__main__":
start = time.time()
with Pool(4) as p: # 4개의 프로세스
results = p.map(cpu_intensive, range(4))
print(f"멀티 프로세싱: {time.time() - start:.2f}초")
2. C 확장 라이브러리 사용
NumPy, Pandas, OpenCV 등의 라이브러리는 내부적으로 C로 구현되어 있고,
연산 중에는 GIL을 해제하기 때문에 진정한 병렬 처리가 가능합니다.
import numpy as np
from concurrent.futures import ThreadPoolExecutor
def numpy_operation(arr):
return np.sum(arr ** 2) # GIL이 해제된 상태로 병렬 실행
arrays = [np.random.rand(1000000) for _ in range(4)]
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(numpy_operation, arrays))
3. asyncio (비동기 프로그래밍)
I/O-bound 작업이 많다면, 스레드 대신 비동기 프로그래밍을 사용하는 것이 더 효율적입니다.
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = ["https://google.com"] * 5
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks)
asyncio.run(main())
Python 3.13과 GIL의 미래
IMPORTANT
Python 3.13부터 실험적으로 GIL을 비활성화할 수 있습니다! (PEP 703)
빌드 시 --disable-gil 옵션을 사용하면 Free-threaded Python을 사용할 수 있습니다.
아직 실험 단계이지만, 향후 Python의 멀티스레딩 성능이 크게 개선될 것으로 기대됩니다.
결론
GIL을 Python의 단점으로만 볼 것이 아니라, 왜 존재하는지 이해하고 상황에 맞는 해결책을 선택하는 것이 중요합니다.