본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 효율적인 병렬 처리를 위한 ProcessPoolExecutor와 ThreadPoolExecutor의 2가지 핵심 내부 통신 방식(IPC) 이해와 해결 방법

by Papa Martino V 2026. 2. 25.
728x90

ProcessPoolExecutor와 ThreadPoolExecutor
ProcessPoolExecutor와 ThreadPoolExecutor

 

파이썬으로 고성능 애플리케이션을 개발할 때, 우리는 필연적으로 '병렬성(Parallelism)''동시성(Concurrency)'이라는 벽에 부딪힙니다. 특히 concurrent.futures 모듈에서 제공하는 ProcessPoolExecutorThreadPoolExecutor는 이를 해결하기 위한 가장 강력한 도구입니다. 하지만 많은 개발자가 이 두 실행기의 외형적 사용법에만 집중할 뿐, 내부적으로 데이터가 어떻게 이동하는지, 즉 IPC(Inter-Process Communication)와 메모리 공유 모델에 대해서는 간과하곤 합니다. 내부 통신 방식을 제대로 이해하지 못하면 대용량 데이터를 처리할 때 예상치 못한 성능 저하나 PicklingError 같은 런타임 오류를 해결하는 데 어려움을 겪게 됩니다. 본 가이드에서는 전문가의 시각에서 두 실행기의 하부 구조를 심층 분석하고, 상황에 맞는 최적의 선택 기준을 제시합니다.


1. ThreadPoolExecutor: 공유 메모리를 통한 경량 통신

ThreadPoolExecutor는 동일한 프로세스 내에서 여러 스레드를 생성하여 작업을 수행합니다. 스레드들은 프로세스의 힙(Heap) 메모리 영역을 공유하기 때문에 별도의 복잡한 IPC 메커니즘이 필요하지 않습니다.

  • 통신 방식: 메모리 직접 참조 (Shared Memory)
  • 장점: 데이터를 복사할 필요가 없어 오버헤드가 매우 적고 빠릅니다.
  • 단점: 파이썬의 GIL(Global Interpreter Lock)로 인해 CPU 연산 집약적인 작업에서는 병렬 처리의 이점을 얻기 어렵습니다.

2. ProcessPoolExecutor: 객체 직렬화와 IPC의 결합

ProcessPoolExecutor는 완전히 독립된 개별 프로세스를 생성합니다. 각 프로세스는 자신만의 메모리 공간을 가지므로, 데이터를 주고받기 위해서는 운영체제 수준의 통신이 필요합니다. 파이썬은 이를 위해 multiprocessing 모듈의 QueuePipe를 내부적으로 사용하며, 객체를 전송하기 위해 pickle을 통한 직렬화 과정을 거칩니다.

  • 통신 방식: Pickle 기반 직렬화 후 OS Pipe/Socket을 통한 전송
  • 장점: GIL의 제약을 받지 않아 멀티코어 CPU 자원을 100% 활용할 수 있습니다.
  • 단점: 객체를 직렬화하고 역직렬화하는 과정에서 상당한 CPU 연산과 시간 비용(Overhead)이 발생합니다.

3. 한눈에 비교하는 Executor 내부 작동 메커니즘

두 방식의 구조적 차이를 표를 통해 명확히 비교해 보겠습니다.

비교 항목 ThreadPoolExecutor ProcessPoolExecutor
기본 단위 Thread (경량) Process (중량)
주요 통신 방식 메모리 주소 직접 공유 IPC (Pipe, Queue, Pickle)
데이터 전달 비용 거의 없음 (Zero-copy에 가까움) 높음 (직렬화 오버헤드 존재)
GIL 영향 직접적인 영향 받음 영향 없음 (회피 가능)
주요 타겟 작업 I/O Bound (네트워크, 파일 읽기) CPU Bound (연산, 이미지 처리)
안정성 하나의 스레드 오류가 전체 영향 가능 프로세스 격리로 상대적 안전

4. 실전 예제: 상황별 선택 방법 (Sample Example)

다음은 대량의 숫자 리스트를 처리할 때 각 Executor가 어떻게 동작하는지 보여주는 예시 코드입니다.

import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def heavy_computation(n):
    # CPU 집약적 작업 예시
    return sum(i * i for i in range(n))

data = [1000000 + i for i in range(10)]

# 1. ThreadPoolExecutor (I/O에 유리, 여기서는 비효율적)
start = time.time()
with ThreadPoolExecutor() as executor:
    results = list(executor.map(heavy_computation, data))
print(f"Thread Time: {time.time() - start:.4f}s")

# 2. ProcessPoolExecutor (CPU 연산에 유리, 직렬화 발생)
start = time.time()
with ProcessPoolExecutor() as executor:
    results = list(executor.map(heavy_computation, data))
print(f"Process Time: {time.time() - start:.4f}s")

전문가 팁: 데이터의 크기가 수 기가바이트(GB)에 달한다면 ProcessPoolExecutor의 IPC 비용이 연산 이득을 넘어설 수 있습니다. 이럴 때는 multiprocessing.shared_memory를 검토하는 것이 해결 방법입니다.

5. 결론 및 최적화 전략

파이썬 병렬 처리의 핵심은 "통신 비용이 연산 이득보다 작은가?"를 판단하는 것입니다. ThreadPoolExecutor는 통신 비용이 0에 가깝지만 연산 병렬화가 안 되고, ProcessPoolExecutor는 진정한 병렬 연산이 가능하지만 데이터 전달 과정이 무겁습니다. 따라서 단순 I/O 대기 시간이 많은 작업은 스레드를, 복잡한 수치 계산이나 데이터 파싱은 프로세스를 선택하십시오. 만약 프로세스 간 통신에서 병목이 발생한다면, 전달하는 객체의 크기를 최소화하거나 기본 자료형(Primitive types) 위주로 구성하여 직렬화 속도를 높이는 것이 성능 해결의 열쇠입니다.


참고 출처:

  • Python Official Documentation: concurrent.futures — Launching parallel tasks
  • "Fluent Python" by Luciano Ramalho - Chapter 20: Concurrent Executors
  • Python Global Interpreter Lock (GIL) Internal Analysis Report
728x90