파이썬으로 데이터 처리 스크립트 짜다가 "그래, 이건 멀티스레드로 돌리면 빠르겠지" 싶어서 threading.Thread를 4개 띄워본 적, 한 번씩은 있으실 거예요. 그런데 막상 돌려보면 어이없는 결과가 나옵니다. CPU 사용률이 한 코어만 100%를 찍고 나머지는 멀쩡한데, 전체 실행 시간은 싱글 스레드보다 오히려 느려지는 경우도 있죠. 처음 이걸 봤을 때 저도 "내 코드가 잘못됐나" 한참 디버깅했습니다. 범인은 코드가 아니라 GIL(Global Interpreter Lock)이었어요. 오늘은 이 GIL이라는 녀석이 왜 존재하고, 어떤 상황에서 발목을 잡는지, 그리고 우회하는 방법까지 정리해 볼게요.
개념 / 원리
GIL을 한 줄로 설명하자면 "CPython 인터프리터가 한 번에 하나의 스레드만 파이썬 바이트코드를 실행하도록 잠가두는 잠금장치"입니다. 비유하자면 4차선 도로처럼 보이지만, 실제로는 통행권을 한 명만 가질 수 있는 1차선 도로인 셈이죠.
왜 이런 게 있을까요? 핵심은 CPython의 레퍼런스 카운팅 메모리 관리 방식에 있습니다. 파이썬 객체마다 "지금 몇 군데서 참조 중인지" 숫자를 들고 다니는데, 이 카운터를 여러 스레드가 동시에 건드리면 race condition이 터집니다. 객체가 0이 됐다고 잘못 판단해서 해제했는데 다른 스레드가 그걸 쓰고 있으면? 세그폴트죠.
이걸 객체마다 락으로 보호하면 락 자체의 오버헤드가 너무 큽니다. 그래서 90년대 초 귀도가 "그냥 인터프리터 전체에 락 하나 걸자"고 결정한 게 GIL의 시작입니다. 단순하고 빠르고, 단일 스레드 성능에는 손해가 없거든요.
문제는 멀티 스레드 CPU bound 작업입니다. 코어가 8개라도 한 번에 하나만 바이트코드를 돌리니, 스레드를 늘려도 속도가 안 올라갑니다. 오히려 스레드 간 GIL을 뺏고 뺏기는 context switching 비용이 더해져서 느려질 수도 있어요.
다만 I/O 대기 중에는 GIL이 풀립니다. 네트워크 응답을 기다리거나 디스크 읽기를 기다리는 스레드는 GIL을 놓고 잠들고, 그 사이 다른 스레드가 일할 수 있어요. 그래서 I/O bound 작업은 멀티 스레드로도 잘 빨라집니다. 이게 핵심 갈림길입니다.
| 작업 유형 | GIL 영향 | 추천 방식 |
|---|---|---|
| CPU bound (계산, 인코딩, 압축) | 큼 — 스레드 의미 없음 | multiprocessing, concurrent.futures.ProcessPoolExecutor |
| I/O bound (HTTP, DB, 파일) | 적음 — 스레드 효과 있음 | threading, asyncio |
| C 확장 호출 (numpy 등) | 코드에 따라 다름 | numpy 같은 라이브러리는 내부에서 GIL을 풀고 동작 |
실전 예제
말로만 들으면 와닿지 않으니 직접 돌려봅시다. 간단한 CPU bound 작업(소수 카운트)을 싱글 스레드, 멀티 스레드, 멀티 프로세스로 각각 돌려서 시간을 재 볼게요.
import time
from threading import Thread
from multiprocessing import Process
def count_primes(start, end):
count = 0
for n in range(start, end):
if n < 2:
continue
is_prime = True
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
is_prime = False
break
if is_prime:
count += 1
return count
def run_single():
t = time.time()
count_primes(2, 200_000)
print(f"single : {time.time() - t:.2f}s")
def run_threads():
t = time.time()
threads = [Thread(target=count_primes, args=(2, 100_000)),
Thread(target=count_primes, args=(100_000, 200_000))]
for th in threads: th.start()
for th in threads: th.join()
print(f"threads : {time.time() - t:.2f}s")
def run_processes():
t = time.time()
procs = [Process(target=count_primes, args=(2, 100_000)),
Process(target=count_primes, args=(100_000, 200_000))]
for p in procs: p.start()
for p in procs: p.join()
print(f"procs : {time.time() - t:.2f}s")
if __name__ == "__main__":
run_single()
run_threads()
run_processes()
제 노트북에서 돌려보면 대략 이런 식의 결과가 나옵니다(머신마다 다르지만 경향은 비슷합니다).
single : 1.80s
threads : 1.95s ← 오히려 느려짐
procs : 1.00s ← 거의 절반스레드는 GIL 때문에 동시 실행이 안 되니 도움이 안 됩니다. 오히려 GIL을 주고받는 비용으로 더 느려져요. 프로세스는 각자 인터프리터를 따로 갖기 때문에 GIL이 무력화되고, 실제로 두 코어를 다 씁니다.
반대로 I/O bound는 그림이 완전히 다릅니다. requests.get()을 10번 순차 호출하면 10초, 스레드 10개로 뿌리면 1초대로 끝납니다. 네트워크 대기 중에는 GIL이 풀리니까요.
주의할 점
1) "스레드를 쓰면 무조건 느리다"는 오해. I/O bound라면 스레드도 충분히 빠릅니다. CPU bound가 아닌 한 굳이 멀티프로세스로 갈 필요 없어요. 어떤 작업인지부터 따져보는 게 먼저입니다.
2) numpy/pandas 등은 GIL을 풉니다. numpy의 벡터 연산은 C 레벨에서 도는 동안 GIL을 해제합니다. 그래서 numpy 위주 계산은 멀티 스레드로도 가속이 됩니다. "내 코드가 CPU bound인데 왜 스레드로 빨라지지?" 싶다면 보통 이 케이스예요.
3) 멀티프로세스는 공짜가 아닙니다. 프로세스 생성·메모리 복사·IPC 비용이 있어서, 작업이 너무 짧으면 오버헤드가 더 큽니다. 그리고 윈도우에서는 if __name__ == "__main__": 가드를 빼먹으면 무한 spawn으로 노트북이 멈춥니다. 저는 이거 모르고 화면을 한 번 박살 낸 적이 있어요.
4) asyncio도 GIL의 영향을 받습니다. 단일 스레드 이벤트 루프라서 결국 GIL과 같은 제약 안에 있어요. 다만 협력적 스케줄링이라 context switching 비용이 훨씬 적습니다. I/O bound + 동시성이 매우 많은 케이스(예: 1만 개 HTTP 요청)에서는 asyncio가 스레드보다 유리합니다.
5) Python 3.13에 free-threaded(no-GIL) 빌드가 실험적으로 등장했습니다. GIL을 없앤 빌드가 옵션으로 제공되는데, 아직은 호환성 문제와 단일 스레드 성능 저하가 있어서 "당장 프로덕션에 써라"는 단계는 아닙니다. 하지만 흐름은 분명히 "언젠가는 GIL이 없는 파이썬"으로 가고 있어요.
마무리
정리하면 세 줄입니다.
- GIL은 CPython이 한 번에 한 스레드만 바이트코드를 돌리도록 거는 잠금장치, 레퍼런스 카운팅 보호가 목적이다.
- CPU bound는
multiprocessing, I/O bound는threading/asyncio— 작업 유형부터 보고 도구를 고른다. - 3.13의 free-threaded 빌드가 시작점이지만, 당분간 실무 코드는 GIL을 전제로 짜는 게 안전하다.
'IT·리눅스 > Python' 카테고리의 다른 글
| 제너레이터 vs 이터레이터 - Python yield는 언제 쓰나 (0) | 2026.07.09 |
|---|---|
| dataclass vs Pydantic - 언제 뭘 써야 할까 (0) | 2026.06.23 |
| multiprocessing vs threading - GIL 때문에 헷갈리는 두 모듈 정리 (0) | 2026.06.09 |
| 데코레이터 만들기와 활용 - @ 한 줄 뒤에 숨은 클로저 (0) | 2026.06.03 |
| asyncio - 코루틴은 GIL을 어떻게 피해 다니는가 (0) | 2026.05.27 |