C.W.K.
Stream
Lesson 04 of 05 · published

Multiprocessing — CPU 바운드의 진짜 병렬성

~18 min · multiprocessing, process, parallel, executor

Level 0호기심
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

왜 multiprocessing 존재

GIL 이 두 스레드가 Python 바이트코드 병렬 실행 막음. Multiprocessing 이 별 Python 프로세스 사용해서 우회 — 각자 자기 GIL, 자기 메모리, 자기 모든 거. 진짜 병렬화 원하는 CPU 바운드 작업엔 이게 답.

비용 — 프로세스 시작 + 직렬화

프로세스 시작이 스레드 시작보다 훨씬 느림 (밀리초 vs 마이크로초). 프로세스 간 데이터 전달엔 *직렬화* 필요 — Python 객체가 경계 통과 시 pickle/unpickle. 그래서 multiprocessing 이 task 당 작업이 시작 + 통신 오버헤드보다 클 때 이김. 1 초 계산엔 오버헤드 무시 가능. 1ms task 엔 threading 또는 동시성 X 가 빠름.

ProcessPoolExecutor — 쉬운 길

concurrent.futures.ProcessPoolExecutor 가 ThreadPoolExecutor 와 같은 API. 그냥 클래스 swap. 함수와 인자가 picklable 해야 — top level 에 정의 (다른 함수 안 X), 인자가 표준 타입 (lambda X, 파일 핸들 X 등). 첫 시도에 사람들 잡힘.

state 공유 — Manager 와 shared memory

프로세스가 디폴트로 메모리 공유 X. multiprocessing.Manager 가 프로세스 간 작동하는 공유 list/dict 의 proxy 줘 (오버헤드 있음). multiprocessing.shared_memory (3.8+) 가 고처리량 숫자 데이터 위한 raw shared memory 블록. 대부분 multiprocessing 코드는 이거 안 필요 — 인자 in, 결과 out, 공유 state X.

원칙: CPU 바운드엔 multiprocessing. I/O 바운드엔 asyncio (또는 threading). 잘못된 선택이 가장 흔한 production 실수 — multiprocessing 으로 HTTP 서버 돌리거나 asyncio 로 CPU 무거운 계산 같은.

Code

ProcessPoolExecutor — 같은 API, 다른 모델·python
from concurrent.futures import ProcessPoolExecutor
import time

def compute_squares(n):
    return sum(i * i for i in range(n))

if __name__ == "__main__":           # 일부 플랫폼에서 multiprocessing 에 필수
    start = time.perf_counter()
    with ProcessPoolExecutor(max_workers=4) as pool:
        results = list(pool.map(compute_squares, [1_000_000] * 4))
    print(f"결과 개수: {len(results)}")
    print(f"경과: {time.perf_counter() - start:.2f}s")
# 다중 코어 머신에서 4 프로세스로 단일 프로세스나
# (GIL 때문에) ThreadPoolExecutor 의 같은 거보다 ~4x 빠름
if __name__ == '__main__': — 왜 중요·python
# Windows 와 macOS-spawn 에서 multiprocessing 이 각 worker 에서
# 스크립트 import. if __name__ guard 없으면 자식 worker 가
# multiprocessing setup 재실행 + 재귀 spawn.
#
# 항상 top-level multiprocessing 코드 guard:
#
# def work(x):
#     return x * 2
#
# if __name__ == '__main__':
#     with ProcessPoolExecutor() as pool:
#         results = list(pool.map(work, range(10)))
#         print(results)

# 함수가 picklable 해야 — 모듈 top level 정의
Pickling — 작동하는 거, 안 되는 거·python
from concurrent.futures import ProcessPoolExecutor

# 작동 — 모듈 레벨 함수, picklable 인자
def double(x):
    return x * 2

# 작동 X — lambda 가 picklable 아님
# squarer = lambda x: x * x
# pool.submit(squarer, 5)         # PicklingError

# 우회 — 모듈 레벨에 함수 정의, 또는 functools.partial 사용
# import functools
# def multiply(a, b):
#     return a * b
# squarer = functools.partial(multiply, 2)   # picklable

if __name__ == '__main__':
    with ProcessPoolExecutor() as pool:
        results = list(pool.map(double, [1, 2, 3, 4]))
        print(results)                # [2, 4, 6, 8]
multiprocessing 안 쓸 때·python
# 나쁜 use case — 오버헤드 지배
#
# 1. 작은 task. pickle + IPC + 프로세스 시작 오버헤드가
#    작업 자체 훨씬 초과.
#
# 2. I/O 바운드 작업. asyncio 또는 threading 이 훨씬 적은 오버헤드로.
#
# 3. 공유 state 필요한 어디든. multiprocessing 이 그거 어렵게 함.
#    asyncio + lock, 또는 공유 state 없이 디자인.
#
# 좋은 use case:
# 1. 병렬화 가능한 숫자 작업 — ML 전처리, 이미지 처리,
#    과학 시뮬레이션
# 2. 각 >>100ms 인 독립 CPU 무거운 task
# 3. 진짜 OS 레벨 병렬성 필요 (threading 과 다름)

# 오늘의 편향 — I/O 엔 asyncio 먼저 시도. multiprocessing 은
# CPU 바운드 병렬 작업에 특정해서.

External links

Exercise

def fib(n) 정의 — 재귀 피보나치 (일부러 느림 — 캐싱 X). ProcessPoolExecutor 로 fib(35), fib(36), fib(37), fib(38) 병렬 계산. 시간. 그 다음 sequential 버전 시간. 비교. (if __name__ == '__main__': guard 필요.) 다중 코어 머신에서 병렬 버전이 ~4x 빠름.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.