
파이썬 개발자가 성능 최적화의 문턱에서 반드시 마주하게 되는 질문이 있습니다. "병렬 처리를 위해 Threading을 써야 할까, 아니면 Multiprocessing을 써야 할까?" 이 질문에 대한 답은 단순히 '둘 다 병렬 처리를 지원한다'는 수준에서 그쳐서는 안 됩니다. 특히 연산 집약적인 CPU 바운드(CPU-bound) 작업에서는 파이썬의 독특한 제약 사항인 GIL(Global Interpreter Lock)에 대한 깊은 이해가 필수적입니다. 본 포스팅에서는 CPU 바운드 작업에서 왜 멀티스레딩이 힘을 쓰지 못하는지, 그리고 멀티프로세싱이 어떻게 진정한 병렬성을 확보하여 성능 병목을 해결하는지 상세히 분석합니다. 실무에서 즉시 활용 가능한 7가지 고성능 병렬 처리 예제와 함께 최적의 아키텍처 설계 방법을 제시합니다.
1. Threading vs Multiprocessing: CPU 관점의 성능 비교
두 라이브러리는 동시성(Concurrency)과 병렬성(Parallelism)을 구현하지만, 메모리 공유 방식과 GIL의 영향도에서 극명한 차이를 보입니다.
| 비교 항목 | Threading (멀티스레딩) | Multiprocessing (멀티프로세싱) |
|---|---|---|
| GIL 영향 | 매우 큼 (한 번에 하나의 스레드만 실행) | 없음 (프로세스당 개별 GIL 보유) |
| 메모리 구조 | 프로세스 내 메모리 공유 (경량) | 독립된 메모리 공간 (무겁고 복사 필요) |
| CPU 바운드 효율 | 낮음 (오히려 컨텍스트 스위칭 비용으로 하락) | 매우 높음 (다중 코어 활용 가능) |
| I/O 바운드 효율 | 높음 (대기 시간에 다른 스레드 실행) | 보통 (오버헤드가 상대적으로 큼) |
| 데이터 통신 | 전역 변수 직접 접근 가능 (Lock 필요) | IPC (Queue, Pipe) 필요 |
2. 왜 CPU 바운드 작업에는 Multiprocessing인가?
파이썬(CPython)의 GIL은 한 번에 하나의 스레드만 바이트코드를 실행하도록 강제합니다. 따라서 수학적 연산, 데이터 압축, 암호화와 같은 CPU 바운드 작업에서 스레드를 늘리는 것은 오히려 스레드 간 주도권 경쟁을 유발하여 성능을 저하시킵니다. 반면, 멀티프로세싱은 별도의 파이썬 인터프리터를 여러 개 띄우는 방식이므로 코어 수만큼 진정한 병렬 연산이 가능해집니다.
3. 실무 적용을 위한 7가지 고성능 병렬 처리 Example
Example 1: CPU 코어 수를 활용한 수치 연산 병렬화 (ProcessPool)
가장 표준적인 방법으로, Pool 객체를 사용하여 작업을 분산합니다.
from multiprocessing import Pool
import time
def heavy_computation(n):
return sum(i * i for i in range(n))
if __name__ == "__main__":
numbers = [10**7] * 8
start = time.time()
with Pool() as p:
results = p.map(heavy_computation, numbers)
print(f"걸린 시간: {time.time() - start:.2f}초")
Example 2: Threading의 CPU 바운드 실패 사례 확인
멀티스레딩이 연산 작업에서 왜 느린지 직접 확인하는 실험적 코드입니다.
import threading
def cpu_work():
count = 0
for _ in range(10**7):
count += 1
# 스레드 2개를 사용해도 실행 시간은 줄어들지 않고 오히려 늘어날 수 있습니다.
t1 = threading.Thread(target=cpu_work)
t2 = threading.Thread(target=cpu_work)
t1.start(); t2.start()
t1.join(); t2.join()
Example 3: 공유 메모리를 이용한 프로세스 간 데이터 통신 (Array)
멀티프로세싱의 단점인 메모리 분리 문제를 shared_memory로 해결하는 방법입니다.
from multiprocessing import Process, Array
def update_array(shared_arr):
for i in range(len(shared_arr)):
shared_arr[i] = shared_arr[i] ** 2
if __name__ == "__main__":
arr = Array('d', [1.0, 2.0, 3.0, 4.0])
p = Process(target=update_array, args=(arr,))
p.start(); p.join()
print(arr[:])
Example 4: Queue를 활용한 생산자-소비자 패턴
대용량 데이터를 안전하게 전달하며 병렬로 처리하는 견고한 아키텍처입니다.
from multiprocessing import Process, Queue
def worker(q):
while True:
data = q.get()
if data is None: break
# CPU 집약적 처리
_ = [x**2 for x in range(data)]
if __name__ == "__main__":
q = Queue()
p = Process(target=worker, args=(q,))
p.start()
q.put(10**6)
q.put(None)
p.join()
Example 5: ProcessPoolExecutor를 이용한 비동기 병렬 처리
concurrent.futures 모듈을 사용하여 세련된 방식으로 병렬 연산을 수행합니다.
from concurrent.futures import ProcessPoolExecutor
def analyze_chunk(chunk):
return len([x for x in chunk if x % 2 == 0])
data = [range(1000), range(1000, 2000)]
with ProcessPoolExecutor() as executor:
results = list(executor.map(analyze_chunk, data))
Example 6: CPU Affinity 설정을 통한 프로세스 제어
특정 CPU 코어에 프로세스를 고정하여 캐시 효율을 극대화하는 기법입니다.
import os
from multiprocessing import Process
def set_affinity(core_id):
if hasattr(os, "sched_setaffinity"):
os.sched_setaffinity(0, {core_id})
print(f"Process set to core {core_id}")
if __name__ == "__main__":
p = Process(target=set_affinity, args=(0,))
p.start(); p.join()
Example 7: 큰 객체 전달 시 발생하는 Serialization 오버헤드 해결
멀티프로세싱으로 데이터를 넘길 때 피클링(Pickling) 비용을 줄이기 위해 전역 변수와 초기화(initializer)를 활용합니다.
import numpy as np
from multiprocessing import Pool
big_data = None
def init_worker(data):
global big_data
big_data = data
def compute_on_global(idx):
return np.mean(big_data[idx])
if __name__ == "__main__":
data = np.random.rand(10**7)
with Pool(initializer=init_worker, initargs=(data,)) as p:
results = p.map(compute_on_global, range(10))
4. 최종 결정 가이드: 상황별 해결 전략
- 수학 연산, 이미지/비디오 인코딩, 머신러닝 학습: 고민 없이 Multiprocessing을 사용하십시오.
- 웹 크롤링, API 요청 대기, 데이터베이스 I/O: 메모리 효율이 좋은 Threading이나 Asyncio가 적합합니다.
- 주의사항: 멀티프로세싱 도입 시, 프로세스 생성 비용이 실제 연산 비용보다 크지 않은지 반드시 프로파일링해야 합니다.
5. 내용의 출처 및 기술 참조
- Python Software Foundation. "multiprocessing — Process-based parallelism." Python Docs.
- David Beazley. "Understanding the Python GIL." PyCon Talk Series.
- Fluent Python: Clear, Concise, and Effective Programming by Luciano Ramalho.
- High Performance Python by Micha Gorelick and Ian Ozsvald.
'Artificial Intelligence > 60. Python' 카테고리의 다른 글
| [PYTHON] 성능 한계 해결을 위한 Cython과 PyPy 도입 시 2가지 핵심 차이와 최적화 방법 (0) | 2026.03.30 |
|---|---|
| [PYTHON] 거대 루프 내 enumerate()와 zip()의 3가지 오버헤드 분석 및 해결 방법 (0) | 2026.03.30 |
| [PYTHON] 딕셔너리 내부의 비밀 : 해시 충돌과 성능 저하를 방지하는 5가지 핵심 방법 (0) | 2026.03.30 |
| [PYTHON] 고성능 비동기 처리를 위한 asyncio 이벤트 루프의 3가지 핵심 원리와 해결 방법 (0) | 2026.03.30 |
| [PYTHON] 비동기 프로그래밍의 핵심, await 뒤에 올 수 있는 3가지 Awaitable 객체 종류와 활용 방법 (0) | 2026.03.30 |