Featured image of post [Python Cheatsheet] 64. Concurrency - threading/multiprocessing 선택

[Python Cheatsheet] 64. Concurrency - threading/multiprocessing 선택

동시성과 병렬성을 빠르게 선택하기 위한 치트시트입니다. GIL 이해, I/O-bound vs CPU-bound 판단, ThreadPoolExecutor/ProcessPoolExecutor, 데이터 공유 주의점을 최소 예제로 정리합니다.

동시성(concurrency)과 병렬성(parallelism)은 성능 향상의 핵심 도구입니다. 이 치트시트는 GIL 이해, I/O vs CPU 바운드 판단, ThreadPoolExecutor/ProcessPoolExecutor 선택 기준을 정리합니다.

언제 이 치트시트를 보나?

  • 여러 작업을 동시에 처리해서 속도를 올리고 싶을 때
  • “threading vs multiprocessing vs asyncio” 선택이 헷갈릴 때

핵심 패턴

  • I/O-bound(네트워크/파일 대기): ThreadPoolExecutor 또는 asyncio
  • CPU-bound(계산 집약): ProcessPoolExecutor (GIL 우회)
  • 간단한 병렬화: concurrent.futuresexecutor.map() 또는 executor.submit()
  • 데이터 공유는 최소화 → 필요하면 Queue, Lock 사용

최소 예제

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# I/O-bound: ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor
import time

def fetch(url: str) -> str:
    time.sleep(1)  # 네트워크 대기 시뮬레이션
    return f"fetched {url}"

with ThreadPoolExecutor(max_workers=4) as executor:
    urls = ["a", "b", "c", "d"]
    results = list(executor.map(fetch, urls))  # 병렬 실행 (~1초)
    print(results)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# CPU-bound: ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor

def heavy_compute(n: int) -> int:
    return sum(i * i for i in range(n))

if __name__ == "__main__":
    with ProcessPoolExecutor(max_workers=4) as executor:
        nums = [10**6, 10**6, 10**6, 10**6]
        results = list(executor.map(heavy_compute, nums))
        print(results)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# submit + Future
from concurrent.futures import ThreadPoolExecutor, as_completed

def task(n):
    return n * 2

with ThreadPoolExecutor() as executor:
    futures = [executor.submit(task, i) for i in range(5)]
    for future in as_completed(futures):
        print(future.result())

GIL 이해

  • GIL(Global Interpreter Lock): CPython에서 한 번에 하나의 스레드만 파이썬 바이트코드 실행
  • I/O 대기 중에는 GIL 해제 → I/O-bound 작업은 threading이 효과적
  • CPU 연산 중에는 GIL 유지 → CPU-bound 작업은 multiprocessing 필요

자주 하는 실수/주의점

  • ProcessPoolExecutorif __name__ == "__main__": 가드 필수 (Windows)
  • 스레드/프로세스 간 가변 객체 공유는 버그 원인 → 불변 데이터 전달 또는 Queue 사용
  • 프로세스는 메모리 복사 비용이 큼 → 작은 작업에는 오버헤드
  • executor.shutdown(wait=True)with 문 사용 시 자동 호출됨

선택 가이드

상황추천
네트워크 요청 다수asyncio 또는 ThreadPoolExecutor
파일 I/O 다수ThreadPoolExecutor
CPU 계산 집약ProcessPoolExecutor
간단한 백그라운드 작업threading.Thread

관련 링크(공식 문서)