필사 모드: Python GIL & CPython 내부 완전 가이드 2025: Global Interpreter Lock, Bytecode, No-GIL, Subinterpreters, Async 심층 분석
한국어들어가며: Python의 가장 큰 비밀
유명한 문제
"Python은 왜 멀티스레드에서 빠르지 않나요?"
답: **GIL (Global Interpreter Lock)** 때문입니다.
20년 넘게 Python 커뮤니티가 GIL로 싸웠다. 제거하려 했고, 우회하려 했고, 받아들이려 했다. 2024년 **PEP 703**이 승인되며 마침내 **No-GIL Python**이 현실이 되고 있다.
간단한 실험
def cpu_bound():
count = 0
for _ in range(50_000_000):
count += 1
Single thread
start = time.time()
cpu_bound()
print(f"1 thread: {time.time() - start:.2f}s")
Multi thread
start = time.time()
threads = [threading.Thread(target=cpu_bound) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(f"4 threads: {time.time() - start:.2f}s")
**예상**: 4 스레드는 4배 빨라야.
**실제 (CPython 3.12)**:
1 thread: 3.2s
4 threads: 13.8s
**4배 느려졌다**. 왜?
답: **GIL**이 한 번에 **하나의 스레드만** Python 바이트코드를 실행하도록 강제한다.
이 글에서 다룰 것
1. **CPython 아키텍처**: Compiler, bytecode, VM.
2. **GIL의 정체**: 왜 존재하는가.
3. **GIL의 구현**: 내부 메커니즘.
4. **GIL의 영향**: 어떤 작업이 느려지나.
5. **asyncio**: GIL을 우회하는 법.
6. **Multiprocessing**: 별도 프로세스.
7. **PEP 703 (No-GIL)**: 2024년의 혁명.
8. **Subinterpreters (PEP 684)**: 또 다른 접근.
9. **Alternative Pythons**: PyPy, Jython, GraalPy.
1. CPython: 대부분 Python의 실제 모습
Python ≠ CPython
**"Python"** 은 **언어 스펙**. 여러 구현체가 있다:
- **CPython**: C로 작성. 표준, 참조 구현. 99%가 사용.
- **PyPy**: Python으로 작성 (RPython). JIT 컴파일러. 빠름.
- **Jython**: JVM 기반.
- **IronPython**: .NET 기반.
- **GraalPy**: GraalVM 기반.
- **MicroPython**: 임베디드용.
이 글의 "Python GIL"은 **CPython의 GIL**이다. 다른 구현체는 다를 수 있다.
CPython 실행 흐름
Python 코드를 실행하면:
source code (.py)
↓
Lexer (tokens)
↓
Parser (AST)
↓
Compiler (bytecode, .pyc)
↓
Virtual Machine (interpreter loop)
↓
출력
Bytecode
CPython은 **bytecode**를 실행한다. Python code를 **중간 표현**으로 컴파일:
def add(a, b):
return a + b
**Bytecode**:
0 LOAD_FAST a
2 LOAD_FAST b
4 BINARY_OP 0 (+)
8 RETURN_VALUE
**`dis` 모듈**로 확인:
def add(a, b):
return a + b
dis.dis(add)
Bytecode Cache
`.pyc` 파일로 저장됨 (`__pycache__/` 디렉토리). 다음 실행 시 재컴파일 생략.
Stack-Based VM
CPython은 **stack-based virtual machine**. 연산은 **stack에서** 일어남:
LOAD_FAST a # a를 stack에 push
LOAD_FAST b # b를 stack에 push
BINARY_OP + # a, b pop, a+b push
RETURN_VALUE # stack top을 반환
**간단하고 이식성 좋다**. 단점: register-based VM보다 조금 느림 (JIT이 덜 유리).
Interpreter Loop
CPython의 심장: **ceval.c**의 `_PyEval_EvalFrameDefault`. **메인 루프**:
for (;;) {
opcode = NEXTOP();
oparg = NEXTARG();
switch (opcode) {
case LOAD_FAST:
// push frame->f_locals[oparg] to stack
DISPATCH();
case BINARY_OP:
// pop two, apply op, push result
DISPATCH();
case RETURN_VALUE:
// return top of stack
...
}
}
**각 bytecode가 C 함수**로 구현됨. 루프가 이를 디스패치.
Python 3.11의 Specializing Adaptive Interpreter
Python 3.11부터 **JIT-like 최적화**:
- **Hot loop 감지**.
- **Specialized bytecode 생성**: 타입 기반 최적화.
- **Quickening**: 실행 중 bytecode 교체.
**예시**: `BINARY_OP`가 두 int를 더하는 경우, **특화된 `BINARY_OP_ADD_INT`** 로 교체. 더 빠름.
이게 Python 3.11의 "25% 더 빠름"의 출처.
**Python 3.13**은 **실험적 JIT** 추가. 본격 JIT은 Python 3.14+ 예정.
2. GIL: 왜 존재하는가
GIL의 정의
**GIL (Global Interpreter Lock)**: **한 번에 하나의 스레드만** Python 바이트코드를 실행할 수 있게 하는 **mutex**.
Thread 1: GIL 획득 → Python code 실행 → GIL 해제
Thread 2: GIL 획득 대기 → ... → GIL 획득 → 실행 → 해제
**모든 Python thread는 GIL을 공유**. 한 번에 하나만.
왜 GIL이 필요한가
**이유 1: Reference Counting의 안전성**
CPython의 메모리 관리는 **reference counting**:
x = [1, 2, 3] # refcount = 1
y = x # refcount = 2
del y # refcount = 1
del x # refcount = 0 → 메모리 해제
각 객체에 `ob_refcnt` 필드. 참조할 때 증가, 해제할 때 감소. 0이 되면 객체 삭제.
**문제**: Multi-thread에서 **ob_refcnt 업데이트**가 race condition. 두 스레드가 동시에 increment → 잘못된 값.
**해결 옵션**:
**A. 모든 ob_refcnt를 atomic**:
- 매 참조마다 atomic 연산.
- **수백 배 느려질 수 있음** (single-thread조차).
**B. 각 객체마다 lock**:
- 엄청난 lock 오버헤드.
- Deadlock 위험.
**C. 하나의 전역 lock (GIL)**:
- Single-thread는 영향 없음.
- Multi-thread는 직렬화.
- 구현 단순.
Python은 **C**를 선택. 이것이 GIL.
**이유 2: C extension의 안전성**
많은 C extension이 **thread-safe하지 않다**. NumPy, scikit-learn 등 초기 버전은 특히. GIL이 있으면:
- C extension 개발자는 **thread safety 걱정 안 해도**.
- Python 런타임이 보호.
**이유 3: 단순성**
Python의 설계 철학: **"simple is better than complex"**.
Fine-grained locking은 복잡. GIL은 단순. Guido가 이를 선택한 이유 중 하나.
GIL의 역사
**1992년**: Guido가 Python 0.9.8에 GIL 도입. "임시 조치". 다시 안 뺐음.
**2000년대 초**: "GIL 없애자" 시도 여러 번. 모두 실패. 이유: Single-thread 성능 감소.
**2010**: Unladen Swallow (Google). JIT + no-GIL 시도. 실패.
**2015**: Larry Hastings의 "Gilectomy". 수년 작업. Single-thread가 2배 느려져 폐기.
**2023**: Sam Gross의 **PEP 703**. 새로운 접근. **승인됨**. Python 3.13에 실험적 포함. Python 3.14에 기본 가능성.
3. GIL의 구현
3.12 이전: Check Intervals
**Python 3.2 이전**: **N개의 bytecode마다** GIL 해제 시도.
**Python 3.2+**: **시간 기반**. 기본 **5 ms** (`sys.setswitchinterval(0.005)`).
**로직**:
while True:
execute_bytecode()
if time_since_last_release > 5ms:
release_GIL()
yield_to_other_thread()
acquire_GIL()
Voluntary vs Forced Release
**Voluntary release**:
- **I/O 작업 시**: `open()`, `socket.recv()` 등. C 함수 호출 전에 GIL 해제.
- **`time.sleep()`**: GIL 해제.
- **긴 C 연산**: NumPy가 이를 적극 활용.
**Forced release**:
- 5 ms 타이머 경과.
- CPython이 **강제로** GIL 해제.
GIL Switching 비용
Context switch 비용:
- Thread state 저장/복원.
- GIL 획득 대기.
- Cache pollution.
**~수 μs 오버헤드**. 많이 switch하면 누적.
스케줄링의 함정
**Python 3.2 이전의 문제**:
CPU-bound 스레드 + I/O-bound 스레드:
- I/O 스레드가 wake up.
- CPU 스레드가 GIL 보유.
- I/O 스레드가 **100 bytecode 기다림** (~1 ms).
- 응답성 저하.
**Python 3.2 해결**: **시간 기반** + **GIL 요청 알림**. I/O 스레드가 GIL을 요청하면 **즉시 CPU 스레드가 양보**.
완벽하진 않지만 크게 개선.
4. GIL의 영향
CPU-bound Multi-thread
**가장 큰 피해자**:
def heavy_computation():
total = 0
for i in range(10_000_000):
total += i * i
return total
threads = [threading.Thread(target=heavy_computation) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
**4개의 스레드**, **1개의 CPU만 사용**. **직렬화**된다. 멀티코어 이점 없음.
**해결**: `multiprocessing` 또는 C extension.
I/O-bound Multi-thread
**거의 영향 없음**:
def fetch(url):
return requests.get(url)
threads = [threading.Thread(target=fetch, args=(url,)) for url in urls]
for t in threads: t.start()
for t in threads: t.join()
**I/O 대기 중엔 GIL 해제**. 여러 HTTP 요청이 **병렬로** 진행.
**이점**:
- I/O 대기 시간 활용.
- N개 요청 ≈ 가장 느린 하나의 시간.
C Extension
**GIL 외부에서** 작업 가능:
**NumPy**:
이 연산 중엔 GIL 해제됨
result = np.dot(large_matrix1, large_matrix2)
NumPy C 코드가 `Py_BEGIN_ALLOW_THREADS` 매크로로 GIL 해제. BLAS가 실행되는 동안 **다른 Python 스레드도 작동**.
이것이 NumPy가 Python에서 **고성능 수치 계산**을 가능하게 하는 이유.
**scikit-learn, Pandas**도 마찬가지.
Locking
**GIL이 있어도** Python 객체는 thread-safe 아님:
counter = 0
def increment():
global counter
for _ in range(1_000_000):
counter += 1 # NOT atomic!
threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # < 4_000_000 (race condition)
**왜?**: `counter += 1`은:
1. `LOAD_GLOBAL counter`
2. `LOAD_CONST 1`
3. `BINARY_OP +`
4. `STORE_GLOBAL counter`
4개 bytecode. GIL switching이 중간에 일어나면 lost update.
**해결**: `threading.Lock()` 사용.
**결론**: GIL은 **interpreter를 보호**하지만 **사용자 코드를 보호하지 않는다**. 여전히 락이 필요.
5. asyncio: GIL을 우회하는 우아한 방법
Event Loop Pattern
**asyncio**: Python의 **동시성 라이브러리**. 단일 스레드에서 여러 I/O 작업 동시 처리.
async def fetch(url):
... I/O 작업 ...
await asyncio.sleep(1)
return url
async def main():
results = await asyncio.gather(
fetch("url1"),
fetch("url2"),
fetch("url3"),
)
asyncio.run(main())
**핵심**: **하나의 스레드**에서 **여러 coroutine**이 협력적으로 실행.
Coroutines
**Coroutine**: 일시 중단과 재개가 가능한 함수.
async def my_coro():
print("start")
await asyncio.sleep(1) # 여기서 일시 중단
print("end")
**`await`**: "여기서 멈추고 다른 일 해도 됨".
Event Loop의 작동
[ready queue]
→ task1
→ task2
→ task3
Event loop:
while True:
task = ready_queue.pop()
resume task
task runs until next `await`
if task awaits I/O:
register I/O with selector
continue
else:
task done
if ready queue empty:
wait for I/O events (selector.select())
when I/O ready, add corresponding tasks to ready queue
GIL과 asyncio
**단일 스레드**라서 GIL 문제 없음. 모든 coroutine이 같은 스레드에서 순차 실행. 하지만 I/O 대기는 효율적.
**장점**:
- **단일 스레드**: GIL 무관.
- **저 비용**: 스레드보다 coroutine이 훨씬 가벼움.
- **수천 concurrent I/O**.
**단점**:
- **CPU-bound 불가**: 여전히 단일 스레드.
- **async 전염**: 한 번 async면 전파.
- **복잡도**: Sync보다 어려움.
실전 예시
**100,000 HTTP 요청**:
**Sync (threads)**:
- 수백 스레드.
- GIL 영향 (약간).
- 수 분.
**Async (asyncio)**:
- 1 스레드.
- 수만 coroutine.
- **수 초~수십 초**.
**수십 배 빠름**. I/O-bound 작업의 표준.
라이브러리
**aiohttp**: HTTP client/server (async).
**asyncpg**: PostgreSQL async.
**aioboto3**: AWS async.
**FastAPI**: Web framework.
**Uvicorn**: ASGI server.
현재 Python async 생태계는 **매우 성숙**하다.
6. Multiprocessing: 별도 프로세스
기본 아이디어
**Multiprocessing**: Python을 **여러 프로세스**로 실행. 각 프로세스가 자기 GIL.
from multiprocessing import Process
def heavy(x):
return sum(i*i for i in range(x))
processes = [Process(target=heavy, args=(10_000_000,)) for _ in range(4)]
for p in processes: p.start()
for p in processes: p.join()
**4 프로세스 → 4 CPU 완전 활용**. 스레드와 달리 GIL 영향 없음.
프로세스 vs 스레드
| 항목 | Thread | Process |
|---|---|---|
| **GIL** | 영향 있음 | 영향 없음 |
| **Memory** | 공유 | 격리 |
| **Overhead** | 작음 | 큼 |
| **Communication** | Shared memory | IPC, pipes, queues |
| **장애** | 공유 (프로세스 크래시) | 격리 |
IPC (Inter-Process Communication)
프로세스 간 데이터 교환:
**Queue**:
from multiprocessing import Process, Queue
q = Queue()
def worker(q):
q.put("hello")
p = Process(target=worker, args=(q,))
p.start()
print(q.get()) # "hello"
p.join()
**Pipe**: 1:1.
**Manager**: 동기화된 데이터 구조.
**SharedMemory** (3.8+): Numpy 배열 등 대용량 공유.
Multiprocessing Pool
from multiprocessing import Pool
def process_item(item):
return item * 2
with Pool(4) as pool:
results = pool.map(process_item, range(1000))
**4개 프로세스**가 작업 분배.
함정
**1. Pickle 문제**:
Process 간 데이터는 **pickle**로 직렬화. Lambda, nested function 등은 pickle 불가.
p = Pool(4)
p.map(lambda x: x*2, data) # ERROR: can't pickle lambda
**해결**: 모듈 레벨 함수 사용.
**2. 메모리**:
각 프로세스가 자기 메모리. **4 프로세스 = 4배 메모리**.
대용량 데이터는 SharedMemory 사용.
**3. 시작 오버헤드**:
Fork (Linux): 빠름.
Spawn (Windows, macOS 기본): 느림. 새 Python 시작.
from multiprocessing import set_start_method
set_start_method('fork')
**4. 디버깅**:
멀티 프로세스 디버깅 어려움. 로그가 섞임.
7. PEP 703: No-GIL Python
역사적 전환점
**2022년**: Facebook의 **Sam Gross**가 "nogil" 프로토타입 공개. 성공적.
**2023년 7월**: **PEP 703** 승인. Python steering council이 동의.
**2024년 Python 3.13**: 실험적 빌드 (`--disable-gil`).
**Python 3.14+**: Default 전환 계획.
접근 방식
Sam Gross의 nogil은 **다른 전략**:
**1. Immortal Objects**:
- `True`, `False`, `None`, 작은 정수 등은 **영원히** 존재.
- Refcount 변경 불필요.
**2. Deferred Reference Counting**:
- 지역 변수의 참조는 **deferred**.
- 함수 종료 시 한 번에 처리.
**3. Biased Reference Counting**:
- 각 객체에 **소유자 스레드**.
- 같은 스레드면 **atomic 아님** (빠름).
- 다른 스레드면 atomic.
**4. Per-Object Locks**:
- 객체마다 자체 lock.
- Critical section만 lock.
**5. Fine-grained Locking**:
- 기존 GIL 대신 **세밀한 lock**.
성능 영향
**Single-thread**:
- 기존 대비 **~5% 느림**.
- 이전 nogil 시도 (Gilectomy)의 2배 느림보다 훨씬 낫다.
**Multi-thread**:
- **진짜** 병렬.
- CPU 수에 비례한 확장.
**벤치마크 예시**:
CPU-bound, 4 threads
GIL: 4 threads → 1x
No-GIL: 4 threads → 3.8x
API 영향
**대부분 호환**:
- 기존 Python 코드 그대로 작동.
- `threading` 모듈 동일.
**C extension의 영향**:
- **Thread safety 확인 필요**.
- 기존 extension은 **재검증** 해야.
- NumPy, Pandas 등이 작업 중.
전환 일정
**2024 (Python 3.13)**:
- 실험적 빌드.
- `PYTHONGIL=0` 환경 변수.
- Opt-in.
**2025 (Python 3.14)**:
- 성숙.
- 일부 배포판에서 기본.
**2027+ (Python 3.16+)**:
- 전면 기본.
- GIL 버전 유지 (호환성).
8. Subinterpreters (PEP 684)
또 다른 접근
**Subinterpreters**: Python 내부에 **여러 interpreter**.
각 subinterpreter는:
- **자기 메모리 영역**.
- **자기 GIL** (Python 3.12+).
- 완전 격리.
**기존 기능**: Python 3.1부터 있었음. C API만.
**PEP 684**: **각 subinterpreter가 독립 GIL**.
**PEP 734**: Python API (3.12+).
사용
Python 3.13+
from concurrent import interpreters
def run_in_sub():
return 42
interp = interpreters.create()
result = interp.run(run_in_sub)
각 subinterpreter가 독립적으로 실행. **진정한 병렬**.
Multiprocessing과의 차이
**Multiprocessing**:
- 별도 OS 프로세스.
- IPC로 통신.
- 느린 시작.
- 높은 메모리.
**Subinterpreters**:
- 같은 OS 프로세스.
- 공유 C 런타임.
- 빠른 시작.
- 낮은 메모리.
- **Python 객체 공유 불가** (격리).
Subinterpreters는 multiprocessing과 threading의 **중간**.
데이터 교환
**Shared memory**:
interpreters.get_default().channels # 통신 채널
**제약**: Python 객체 직접 공유 불가. pickle 필요 (multiprocessing과 유사).
언제 유용한가
- **웹 서버**: 각 요청이 subinterpreter.
- **플러그인**: 격리된 실행.
- **No-GIL 대안**: 기존 extension과의 호환성.
아직 실험적. 생태계가 적응 중.
9. 실전 성능 가이드
언제 무엇을 쓸 것인가
**CPU-bound + 단일 코어 충분**:
- Sync Python. 단순.
**CPU-bound + 멀티코어 필요**:
- **Multiprocessing** (지금).
- **No-GIL** (미래).
- **C extension** (NumPy 등).
**I/O-bound + 작은 규모**:
- Threading + sync 라이브러리. 간단.
**I/O-bound + 대규모**:
- **asyncio**. 수천~수만 동시.
**Mixed (I/O + CPU)**:
- asyncio + multiprocessing.
- CPU 작업은 `ProcessPoolExecutor`로.
성능 비교
같은 작업, 다른 접근:
**100만 숫자 제곱 합**:
Sync
def sync_sum():
return sum(i*i for i in range(1_000_000))
~100 ms
Threads (GIL 영향)
4 threads: ~100 ms (not faster)
Multiprocessing
4 processes: ~30 ms (~3.3x speedup)
NumPy
def numpy_sum():
arr = np.arange(1_000_000)
return (arr * arr).sum()
~5 ms (20x)
Numba (JIT)
from numba import jit
@jit
def numba_sum():
total = 0
for i in range(1_000_000):
total += i * i
return total
~2 ms (50x)
**교훈**: Python의 "느림"은 **사용 방식**에 달려 있음.
프로파일링
**cProfile**: 내장 프로파일러.
python -m cProfile -o profile.stats script.py
**py-spy**: Rust로 작성. 프로덕션 안전.
py-spy record -o profile.svg -- python script.py
**scalene**: CPU, memory, GPU 프로파일.
**line_profiler**: 줄 단위.
10. 대안 Python 구현체
PyPy
**PyPy**: CPython의 대안. JIT 컴파일러.
**장점**:
- **CPU 집약적 코드에서 10-100배 빠름**.
- API 호환 (대부분).
**단점**:
- C extension 호환성 제한.
- 메모리 사용 약간 많음.
- 시작 시 warm-up.
**언제 사용**: 순수 Python 계산.
Cython
**Cython**: Python + C 타입.
cython
def sum_cython(int n):
cdef int total = 0
cdef int i
for i in range(n):
total += i * i
return total
C로 컴파일. **C 속도**. NumPy, Pandas 등이 내부적으로 사용.
Numba
**Numba**: JIT 데코레이터. 간단.
from numba import jit
@jit(nopython=True)
def fast_function(x):
...
NumPy 친화적. 과학 계산에 인기.
Mojo (미래)
**Mojo**: Modular (Chris Lattner)가 개발 중인 **새 언어**.
- Python과 호환.
- **수천 배 빠름** (주장).
- ML/AI 타겟.
아직 preview. 주목할 만.
퀴즈로 복습하기
**A.**
**답**: NumPy의 **계산 집약적 연산**은 **C 코드로 작성**되어 있고, 그 C 코드 내에서 **GIL을 해제**하기 때문이다.
**상세 설명**:
**CPython의 GIL 규칙**:
- **Python bytecode 실행 중엔** GIL 필요.
- **C 함수 실행 중엔** C 코드가 결정:
- GIL 유지: 짧은 연산, Python 객체 직접 조작.
- GIL 해제: 긴 연산, Python 객체 건드리지 않음.
**NumPy의 전략**:
`numpy.dot(A, B)` 같은 매트릭스 연산은:
1. Python에서 `numpy.dot` 호출.
2. NumPy의 C 함수 진입.
3. **`Py_BEGIN_ALLOW_THREADS`** 매크로로 GIL 해제.
4. 순수 C/BLAS 코드로 계산 수행.
5. **`Py_END_ALLOW_THREADS`** 로 GIL 재획득.
6. Python 결과 객체로 반환.
**핵심**: 3-5 단계 동안 **다른 Python 스레드가 자유롭게** 실행.
**예시**:
def heavy_numpy():
A = np.random.rand(1000, 1000)
B = np.random.rand(1000, 1000)
C = np.dot(A, B) # GIL 해제됨
threads = [threading.Thread(target=heavy_numpy) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
**실제 측정**:
- 1 thread: 1.0 초.
- 4 threads: **0.3 초** (거의 3배 빠름).
**일반 Python 코드**는 같은 조건에서 **4배 느려지는** 것과 대조적.
**왜 이것이 가능한가**:
NumPy 연산의 특징:
- **C 배열** 직접 조작 (Python 객체 아님).
- **BLAS (Basic Linear Algebra Subprograms)** 호출.
- MKL, OpenBLAS 등이 실제 계산.
- **멀티 스레드 안전** (BLAS 자체가 그렇게 설계).
GIL 해제 조건이 완벽히 충족:
1. Python 객체 안 건드림 (C 배열만).
2. 긴 연산 (수 ms+).
3. Re-entrant.
**어떤 NumPy 연산이 GIL 해제**:
**GIL 해제**:
- `np.dot`, `np.matmul`: 행렬 곱.
- `np.add`, `np.multiply`: 큰 배열 연산.
- `np.linalg.solve`, `np.linalg.eig`: 선형대수.
- `np.fft`: FFT.
- `np.sort`: 정렬 (큰 배열).
**GIL 유지 (짧음)**:
- 스칼라 연산.
- 작은 배열 (수백 개 이하).
- 인덱싱 `arr[i]`.
- 타입 변환.
**판단 기준**: 대략 **50 μs 이상** 걸리는 연산은 GIL 해제.
**BLAS와 병렬성**:
BLAS 자체가 **멀티스레드**. 예를 들어 MKL은 OMP (OpenMP)로 여러 CPU 코어 사용.
os.environ['OMP_NUM_THREADS'] = '4'
**이중 병렬성**:
- Python 스레드 레벨: 4개.
- BLAS 스레드 레벨: 각 Python 스레드 × 4 BLAS 스레드 = 16.
**주의**: Over-subscription 위험. CPU 16 core인데 16 × 16 = 256 스레드.
**관리 방법**:
from threadpoolctl import threadpool_limits
with threadpool_limits(limits=1, user_api='blas'):
BLAS는 1 스레드만 사용
np.dot(A, B)
**다른 예**:
**scikit-learn**: 대부분의 알고리즘이 NumPy/Cython 기반. GIL 해제.
**Pandas**: DataFrame 연산도 마찬가지.
**SciPy**: NumPy 기반.
**PIL/Pillow**: 이미지 처리. 일부 GIL 해제.
**OpenCV Python**: 대부분 GIL 해제.
**TensorFlow, PyTorch**: 자체 C++ 런타임. GIL 해제.
**중요한 구분**:
**GIL-friendly Python 코드**:
Pure Python: GIL 영향
for i in range(1_000_000):
total += i * i
**GIL-free NumPy 코드**:
NumPy: GIL 해제
arr = np.arange(1_000_000)
total = (arr * arr).sum()
두 번째가 **GIL 관점에서도, 순수 성능에서도** 훨씬 빠르다.
**"Python이 느리다"의 진짜 의미**:
**느림**:
- 순수 Python 루프.
- 객체 생성 많음.
- 동적 타입 검사.
**빠름 (C 속도)**:
- NumPy 벡터 연산.
- `bytes`, `str` 연산.
- C extension 일반.
**함정**: NumPy를 쓰되 **안 쓰는 방식으로** 쓰면 느림:
느림: 루프
result = np.zeros(1_000_000)
for i in range(1_000_000):
result[i] = arr[i] * 2
빠름: 벡터화
result = arr * 2
NumPy는 **벡터화**가 핵심. 루프로 쓰면 Python의 느림 + NumPy 오버헤드.
**고급 기술: `Py_BEGIN_ALLOW_THREADS`**:
Custom C extension을 작성할 때:
static PyObject *
my_long_func(PyObject *self, PyObject *args) {
// Python 인자 파싱
long n;
if (!PyArg_ParseTuple(args, "l", &n)) return NULL;
long result = 0;
Py_BEGIN_ALLOW_THREADS // GIL 해제
// 긴 C 계산
for (long i = 0; i < n; i++) {
result += heavy_computation(i);
}
Py_END_ALLOW_THREADS // GIL 재획득
return PyLong_FromLong(result);
}
**중요**:
- GIL 해제 중엔 **Python 객체 접근 금지**.
- 로컬 변수, C 함수 호출만.
- GIL 없이 `PyLong_FromLong` 등 호출하면 **크래시**.
**성능 지침**:
CPU-bound 작업이 많다면:
1. **순수 Python**: 느림. GIL 문제.
2. **NumPy**: 빠름. GIL 자유.
3. **Cython**: 더 빠름. GIL 제어 가능.
4. **Numba**: JIT. GIL 옵션.
5. **C extension**: 완전 제어.
6. **Rust + PyO3**: 최신 트렌드.
7. **Multiprocessing**: 별도 프로세스. GIL 없음.
**NumPy가 첫 번째 선택**인 이유:
- 가장 단순.
- 거의 모든 곳에서 빠름.
- GIL도 해결.
**교훈**:
**"Python의 GIL이 싫으면 Python을 덜 써라"**.
모순적이지만 사실이다. CPU 집약적 작업은:
- NumPy 벡터화.
- C 확장.
- 또는 Python 밖 (Rust, Go).
**"Python은 glue language"**. 고수준 로직은 Python, 성능 중요한 부분은 C/C++. 이것이 Python 생태계의 **철학**이며, GIL이 큰 문제 안 되는 이유다.
NumPy, Pandas, TensorFlow, PyTorch 등 **주요 과학/AI 라이브러리는 모두 이 패턴**을 따른다. 그래서 Python이 GIL 있어도 **데이터 과학의 표준**이 되었다.
**당신의 Python 코드가 느리다면**, 먼저 물어보라: "이걸 NumPy로 쓸 수 있나?". 대부분 답은 "예"이고, **10-100배 빨라진다**. GIL은 부차적 문제가 된다.
**당신의 I/O 코드가 느리다면**, asyncio로. 이것도 GIL 영향 없음.
**진짜 순수 Python CPU 병렬성이 필요하다면** (드물다), multiprocessing 또는 No-GIL Python (미래). 그때까지는 대부분의 경우 **NumPy + asyncio** 조합이 답이다.
마치며: Python의 미래
핵심 정리
1. **CPython**: Bytecode interpreter. Stack-based VM.
2. **GIL**: 한 번에 한 스레드. Reference counting 보호.
3. **asyncio**: GIL 우회. I/O-bound 강함.
4. **Multiprocessing**: 별도 프로세스. CPU-bound 해결.
5. **No-GIL (PEP 703)**: 진짜 멀티스레드. Python 3.14+.
6. **Subinterpreters**: 격리된 interpreter 여러 개.
7. **NumPy 등**: C extension이 GIL 해제.
Python이 살아남은 이유
Python은 **1991년**에 시작. 2025년 현재 **가장 인기 있는 프로그래밍 언어** 중 하나. 왜?
**철학**:
- 간단함.
- 가독성.
- "There should be one obvious way".
**생태계**:
- 수만 개의 라이브러리.
- 모든 도메인 커버.
- 활발한 커뮤니티.
**데이터 과학 + AI의 표준**:
- NumPy, Pandas, scikit-learn.
- TensorFlow, PyTorch, JAX.
- Jupyter notebooks.
**GIL에도 불구하고** 성공한 것은, **GIL이 실제로 큰 문제가 되지 않았기** 때문이다:
- I/O-bound: asyncio, threading 충분.
- CPU-bound: NumPy, multiprocessing.
- 대부분의 앱: 단일 스레드 충분.
마지막 교훈
Python은 **완벽한 언어가 아니다**. 느리고 복잡하고 GIL이 있다. 하지만 **가장 접근 가능한 언어** 중 하나다. 초보자도 수 분 만에 의미 있는 코드를 쓴다. 전문가는 C extension으로 극한 성능을 낸다.
이 **유연성이 Python의 진짜 강점**이다. 모든 수준의 개발자가 쓸 수 있고, 모든 종류의 문제를 풀 수 있다.
GIL은 오랜 논란의 대상이었지만, Python의 성공을 막지 못했다. No-GIL이 와도, asyncio와 NumPy가 여전히 주류일 것이다. **도구는 문제에 맞게** 쓰는 것이 중요하다.
당신이 Python을 쓸 때, 이 글의 지식이 다음 질문에 답할 수 있게 도와줄 것이다:
- 왜 내 멀티스레드 코드가 느린가?
- asyncio를 써야 할까?
- Multiprocessing이 필요한가?
- NumPy가 왜 빠른가?
- No-GIL Python은 언제 써야 하나?
이 답들을 알면 **Python을 훨씬 효과적으로** 사용할 수 있다. 언어의 한계를 알면, 그 안에서 최대로 활용할 수 있다.
참고 자료
- [CPython GitHub](https://github.com/python/cpython)
- [CPython Internals (Anthony Shaw)](https://realpython.com/products/cpython-internals-book/)
- [Python's Innards Blog (Yaniv Aknin)](https://tech.blog.aknin.name/category/my-projects/pythons-innards/)
- [PEP 703: Making the GIL Optional](https://peps.python.org/pep-0703/)
- [PEP 684: A Per-Interpreter GIL](https://peps.python.org/pep-0684/)
- [PEP 734: Multiple Interpreters in the Stdlib](https://peps.python.org/pep-0734/)
- [Python docs: threading module](https://docs.python.org/3/library/threading.html)
- [Python docs: asyncio](https://docs.python.org/3/library/asyncio.html)
- [Sam Gross's nogil fork](https://github.com/colesbury/nogil)
- [Real Python: Speed Up Python](https://realpython.com/)
- [David Beazley: Understanding the Python GIL (PyCon talk)](https://www.youtube.com/watch?v=Obt-vMVdM8s)
현재 단락 (1/632)
"Python은 왜 멀티스레드에서 빠르지 않나요?"