GIL(Global Interpreter Lock) 이해
GIL은 CPython 인터프리터에서 한 번에 하나의 스레드만 파이썬 바이트코드를 실행할 수 있도록 보장하는 뮤텍스입니다. 멀티스레드 프로그램에서 동시성의 핵심 제약이자, 올바른 병렬화 전략을 선택하는 출발점입니다.
GIL이란?
import threading
import time
# GIL의 영향을 확인하는 예제
def cpu_bound(n: int) -> int:
"""CPU 집약 작업: 순수 계산"""
count = 0
for i in range(n):
count += i
return count
# 단일 스레드
start = time.perf_counter()
cpu_bound(50_000_000)
cpu_bound(50_000_000)
single_time = time.perf_counter() - start
# 멀티 스레드 (GIL로 인해 기대만큼 빠르지 않음)
start = time.perf_counter()
t1 = threading.Thread(target=cpu_bound, args=(50_000_000,))
t2 = threading.Thread(target=cpu_bound, args=(50_000_000,))
t1.start()
t2.start()
t1.join()
t2.join()
multi_time = time.perf_counter() - start
print(f"단일 스레드: {single_time:.2f}s")
print(f"멀티 스레드: {multi_time:.2f}s")
print(f"속도 향상: {single_time / multi_time:.2f}x (기대값: 2x, 실제: ~1x)")
# CPU-bound 작업은 GIL로 인해 멀티스레드가 거의 효과 없음
I/O-bound vs CPU-bound
import threading
import time
import urllib.request
# I/O-bound 작업: GIL이 해제됨 → 멀티스레드 효과 있음
def io_bound_sleep(seconds: float) -> None:
"""I/O 대기 시뮬레이션"""
time.sleep(seconds) # sleep 중에는 GIL 해제
# 단일 스레드: 순차 실행
start = time.perf_counter()
io_bound_sleep(1.0)
io_bound_sleep(1.0)
io_bound_sleep(1.0)
single_time = time.perf_counter() - start
# 멀티 스레드: I/O 대기 중 다른 스레드 실행
start = time.perf_counter()
threads = [threading.Thread(target=io_bound_sleep, args=(1.0,)) for _ in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
multi_time = time.perf_counter() - start
print(f"I/O-bound 단일 스레드: {single_time:.2f}s")
print(f"I/O-bound 멀티 스레드: {multi_time:.2f}s")
print(f"속도 향상: {single_time / multi_time:.2f}x") # ~3x
# GIL이 해제되는 시점:
# - time.sleep()
# - I/O 작업 (파일 읽기/쓰기, 네트워크)
# - C 확장 함수 (NumPy, OpenCV 등 많은 라이브러리)
GIL 우회 전략
import multiprocessing
import threading
import time
def cpu_bound_task(n: int) -> int:
return sum(range(n))
N = 30_000_000
CORES = 2
# 전략 1: multiprocessing — 각 프로세스마다 독립적인 GIL
# CPU-bound 작업에 적합
start = time.perf_counter()
with multiprocessing.Pool(CORES) as pool:
results = pool.map(cpu_bound_task, [N, N])
mp_time = time.perf_counter() - start
# 전략 2: threading — I/O-bound 작업에 적합
start = time.perf_counter()
results_t = []
def wrapper():
results_t.append(cpu_bound_task(N))
threads = [threading.Thread(target=wrapper) for _ in range(CORES)]
for t in threads: t.start()
for t in threads: t.join()
thread_time = time.perf_counter() - start
print(f"multiprocessing: {mp_time:.2f}s")
print(f"threading (CPU): {thread_time:.2f}s")
GIL 없는 파이썬: Python 3.13 Free-threaded
# Python 3.13+: 실험적 GIL-free 모드
# python3.13t (free-threaded build)로 실행 시 GIL 비활성화 가능
# 확인 방법
import sys
# sys.flags.ignore_environment or 빌드 확인
# python3.13 --version → "Python 3.13.x experimental free-threading build"
is_free_threaded = not getattr(sys.flags, 'gil', True)
print(f"Free-threaded: {is_free_threaded}")
# PEP 703: 3.13에서 실험적, 3.14에서 기본값 변경 예정
# --disable-gil 플래그로 빌드하거나 PYTHON_GIL=0 환경변수 설정
정리: 작업 유형별 최적 전략
# ┌─────────────────┬──────────────────────┬───────────────────────┐
# │ 작업 유형 │ 추천 방법 │ 이유 │
# ├─────────────────┼──────────────────────┼───────────────────────┤
# │ I/O-bound │ threading / asyncio │ GIL 해제, 대기 활용 │
# │ CPU-bound │ multiprocessing │ 프로세스별 독립 GIL │
# │ 대규모 수치연산 │ NumPy / Cython │ C 레벨에서 GIL 해제 │
# │ 비동기 I/O │ asyncio │ 단일 스레드 이벤트 루프 │
# └─────────────────┴──────────────────────┴───────────────────────┘
# 핵심 원칙:
# - "GIL 때문에 멀티스레딩이 느리다" → CPU-bound에만 해당
# - I/O-bound(웹 크롤링, DB 쿼리, 파일 처리)에는 threading이 효과적
# - 진짜 CPU 병렬화는 multiprocessing 또는 ProcessPoolExecutor 사용
print("GIL: CPU-bound → multiprocessing, I/O-bound → threading/asyncio")
정리
| 구분 | GIL 영향 | 추천 도구 |
|---|---|---|
| CPU-bound (순수 계산) | 멀티스레드 무효 | multiprocessing, ProcessPoolExecutor |
| I/O-bound (파일/네트워크) | GIL 해제 → 효과 있음 | threading, asyncio, ThreadPoolExecutor |
| 수치 연산 (NumPy) | C 레벨 GIL 해제 | NumPy, Cython, Numba |
| Python 3.13+ | GIL 선택적 비활성화 | Free-threaded build (실험적) |
GIL은 CPython의 특성입니다. PyPy, Jython, GraalPy 등 다른 구현체에는 GIL이 없거나 다르게 동작합니다.