Skip to content

필사 모드: Python GIL & CPython 내부 완전 가이드 2025: Global Interpreter Lock, Bytecode, No-GIL, Subinterpreters, Async 심층 분석

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며: 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은 왜 멀티스레드에서 빠르지 않나요?"

작성 글자: 0원문 글자: 17,000작성 단락: 0/632