Python GIL(Global Interpreter Lock)은 CPython 인터프리터에서 한 번에 하나의 스레드만 Python 바이트코드를 실행할 수 있게 하는 뮤텍스다. 멀티코어 CPU를 활용한 CPU 집약적 병렬 처리를 어렵게 하는 주요 원인이다.
GIL이 존재하는 이유
이유: CPython의 참조 카운팅(Reference Counting) 메모리 관리 방식
여러 스레드가 동시에 참조 카운트를 수정하면 메모리 오류 발생
GIL이 이를 단순하게 방지
영향:
- I/O 바운드 작업: 거의 영향 없음 (I/O 대기 중 GIL 해제)
- CPU 바운드 작업: 멀티스레딩으로 성능 향상 없음
I/O 바운드 vs CPU 바운드
python
import threading
import multiprocessing
import time
# I/O 바운드: 스레딩 효과적
def io_task():
import urllib.request
urllib.request.urlopen('https://httpbin.org/delay/1')
# CPU 바운드: 스레딩 효과 없음, 멀티프로세싱 필요
def cpu_task(n):
return sum(i * i for i in range(n))
# 스레딩 (I/O 적합)
threads = [threading.Thread(target=io_task) for _ in range(10)]
start = time.time()
for t in threads: t.start()
for t in threads: t.join()
print(f"스레딩: {time.time() - start:.2f}s") # ~1초
# 멀티프로세싱 (CPU 적합)
with multiprocessing.Pool(4) as pool:
results = pool.map(cpu_task, [10**6] * 4)
concurrent.futures
python
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import requests
# ThreadPoolExecutor: I/O 바운드 (GIL 해제됨)
urls = ['https://httpbin.org/get'] * 10
with ThreadPoolExecutor(max_workers=10) as executor:
responses = list(executor.map(requests.get, urls))
# ProcessPoolExecutor: CPU 바운드 (별도 프로세스)
def heavy_math(n):
return sum(i**2 for i in range(n))
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(heavy_math, [10**6] * 4))
asyncio: 협력적 동시성
python
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://httpbin.org/get'] * 10
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks)
asyncio.run(main())
Python 3.12+ GIL 변화
Python 3.12: PEP 703 - GIL 비활성화 옵션 (실험적)
python -X nogil 또는
환경변수 PYTHON_GIL=0
Python 3.13+: nogil 빌드 공식 지원 예정
관련 개념