Python 병렬 처리의 함정 (GIL) 완벽 해결: Multiprocessing 아키텍처
[Troubleshooting] Python 병렬 처리의 함정 (GIL) 완벽 해결: Multiprocessing 아키텍처
"대용량 이미지 1만 장의 해상도를 변환하기 위해 파이썬 threading 모듈을 써서 스레드를 10개나 띄웠습니다. 그런데 실행 시간을 측정해 보니 스레드를 1개 쓸 때랑 속도가 똑같고 심지어 더 느려지기까지 합니다."
다른 언어(Java, C++)를 쓰다가 파이썬으로 넘어온 백엔드 엔지니어들이나 데이터 사이언티스트들이 마주하는 가장 기괴하고 배신감 드는 벤치마크 결과입니다. 멀티 스레딩을 구현했는데 CPU 코어는 단 하나만 100%를 찍고 나머지는 놀고 있습니다.
이 현상은 당신의 코드가 틀려서가 아닙니다. 파이썬 생태계의 심장부에 박혀있는 거대한 자물쇠, GIL(Global Interpreter Lock) 때문입니다. 본 가이드에서는 파이썬이 병렬 처리를 기만하는 태생적인 한계를 해체하고, CPU의 모든 코어를 100% 착취하여 연산 속도를 극단적으로 끌어올리는 멀티 프로세싱(Multiprocessing) 아키텍처를 단호하게 제시합니다.
📌 이 글의 핵심 포인트
- GIL의 본질: 파이썬 객체의 메모리 충돌을 막기 위해 단 하나의 스레드만 파이썬 바이트코드를 실행하도록 강제하는 뮤텍스(Mutex) 락
- I/O Bound vs CPU Bound: 웹 크롤링(네트워크 대기)에는 스레드가 좋지만, 이미지 처리(수학 연산)에는 스레드가 독이 되는 이유
- 완벽한 해결책: GIL의 지배를 받지 않는 독립적인 메모리 공간을 생성하는
ProcessPoolExecutor아키텍처 도입
1단계: 핵심 원인 분석 - 자물쇠에 갇힌 스레드들
파이썬은 메모리를 관리하기 위해 '참조 카운팅' 방식을 씁니다. 여러 스레드가 동시에 같은 변수에 접근하면 메모리에 치명적인 오류가 발생할 수 있으므로, CPython(파이썬 공식 구현체)은 아예 "한 번에 오직 하나의 스레드만 파이썬 코드를 실행할 수 있다"라는 GIL이라는 거대한 자물쇠를 채워버렸습니다.
따라서 스레드를 10개 띄워봤자, 자물쇠 열쇠는 1개뿐이라 9개의 스레드는 대기하고 1개만 실행되는 릴레이 경주를 하게 됩니다. 스레드를 교체(Context Switching)하는 시간만 추가되어 결국 싱글 스레드보다 속도가 더 느려지는 참사가 벌어집니다.
2단계: 완벽한 트러블슈팅 - 프로세스 찢기 (Multiprocessing)
CPU를 혹사시키는 연산(이미지 처리, 머신러닝, 엑셀 파싱 등)을 병렬로 돌리려면, 스레드(Thread)를 늘릴 것이 아니라 파이썬 프로그램(Process) 자체를 여러 개로 쪼개어 복제해야 합니다. 각 프로세스는 독립적인 메모리와 자신만의 GIL을 가지므로 락(Lock) 경합이 발생하지 않습니다.
import time
from concurrent.futures import ProcessPoolExecutor
# CPU를 심하게 사용하는 무거운 연산 함수 (예: 이미지 처리)
def heavy_cpu_task(item):
result = sum(i * i for i in range(10**6))
return f"{item} 처리 완료"
if __name__ == "__main__":
items = range(20)
start_time = time.time()
# ❌ 최악의 안티 패턴: ThreadPoolExecutor를 사용하면 GIL 때문에 느림
# ✅ 무결점 아키텍처: CPU 코어 수만큼 프로세스를 복제하여 병렬 연산 (ProcessPoolExecutor)
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(heavy_cpu_task, items))
print("실행 시간:", time.time() - start_time)
이 아키텍처를 도입하는 순간, 작업 관리자의 CPU 그래프가 모든 코어에서 100%로 치솟으며 연산 시간이 물리적인 코어 개수에 비례하여 극적으로 단축됩니다.
🙋♂️ 자주 묻는 질문 (FAQ)
Q. 그렇다면 파이썬에서 Thread는 아예 쓰면 안 되는 쓰레기인가요?
A. 아닙니다! 웹 크롤링이나 API 통신, 파일 읽기 쓰기처럼 외부 응답을 기다리며 대기하는 작업(I/O Bound)을 할 때는 스레드가 최고입니다. 대기하는 시간 동안에는 GIL 자물쇠를 다른 스레드에게 양보하기 때문입니다. CPU 연산은 프로세스로, 네트워크 대기는 스레드(또는 Asyncio)로 분리하는 것이 아키텍트의 기본 소양입니다.
💡 핵심 정리 및 마무리
파이썬은 사용하기 쉬운 언어지만, 그 내부의 메모리 관리 메커니즘(GIL)을 이해하지 못하면 하드웨어의 스펙을 절반도 쓰지 못하는 비효율의 늪에 빠집니다. 내가 작성하는 코드가 연산 위주(CPU)인지 대기 위주(I/O)인지 명확히 진단하고, 그 성격에 맞춰 Thread와 Process를 정밀하게 스위칭하는 쾌도난마의 엔지니어링을 구사하시기 바랍니다.
댓글
댓글 쓰기