asyncio·스레드·멀티프로세싱, 언제 무엇을 쓸 것인가
세 가지 모델, 세 가지 해결 대상
지금까지 스레드, asyncio, 그리고 스치듯 언급했던 멀티프로세싱을 봤습니다. 셋을 우열로 줄 세우려는 시도는 대부분 틀립니다. 셋은 서로 다른 문제를 풉니다.
| 모델 | 진짜로 병렬 실행되는가 | 적합한 작업 | 대표 도구 |
|---|---|---|---|
| threading | I/O 대기 중엔 사실상 그렇다(GIL이 풀림) | I/O 바운드, 기존 동기 라이브러리를 그대로 써야 할 때 | ThreadPoolExecutor, threading.Thread |
| asyncio | 아니다 — 단일 스레드 협력적 스케줄링 | 대량의 동시 I/O 연결(수백~수만) | asyncio, httpx.AsyncClient |
| multiprocessing | 그렇다 — 별도 프로세스, 별도 GIL | CPU 바운드 연산 | multiprocessing, ProcessPoolExecutor |
CPU 바운드 작업 — 이미지 리사이즈, 대량 데이터 파싱, 수치 연산 — 은 1장에서 본 것처럼 스레드로는 병렬화되지 않습니다. GIL이 한 시점에 하나의 스레드만 바이트코드를 실행하게 강제하기 때문입니다. 이런 작업은 multiprocessing.Pool 이나 concurrent.futures.ProcessPoolExecutor 로 여러 프로세스에 나눠야 실제 코어 수만큼 병렬성을 얻습니다.
from concurrent.futures import ProcessPoolExecutor
def cpu_heavy(n: int) -> int:
return sum(i * i for i in range(n))
with ProcessPoolExecutor(max_workers=4) as pool:
results = list(pool.map(cpu_heavy, [10_000_000] * 4))각 프로세스는 독립된 파이썬 인터프리터와 독립된 GIL을 가지므로 이번엔 진짜로 4개 코어를 동시에 씁니다. 대가는 있습니다 — 프로세스 간 데이터 전달은 pickle 로 직렬화/역직렬화해야 하고, 프로세스 생성 자체의 오버헤드도 스레드나 코루틴보다 훨씬 큽니다. 작업 단위가 아주 작다면(예: 숫자 하나 더하기) 오히려 프로세스 생성 비용이 더 클 수 있습니다.
판단 기준을 하나만 남긴다면
"이 작업이 기다리는 시간이 대부분인가, 계산하는 시간이 대부분인가"입니다. 네트워크 응답을 기다리거나 디스크 I/O를 기다리는 시간이 대부분이면 asyncio나 스레드입니다. CPU가 쉬지 않고 도는 시간이 대부분이면 멀티프로세싱입니다. 그 다음 asyncio와 스레드 사이의 선택은 규모의 문제입니다 — 동시 연결 수가 스레드로 감당되는 수준(대략 수십~수백)이면 스레드가 코드도 단순하고 기존 동기 라이브러리 생태계를 그대로 쓸 수 있어 실용적입니다. 수천 개 이상으로 올라가면 asyncio 외에는 대안이 마땅치 않습니다.
flowchart TD A["작업이 대부분 기다리는 시간인가, 계산하는 시간인가"] -->|계산| B["multiprocessing"] A -->|대기| C["동시 연결 수가 수백 이하인가"] C -->|예| D["threading — 기존 동기 라이브러리 그대로 사용 가능"] C -->|아니오| E["asyncio — 코루틴은 스레드보다 훨씬 가볍다"]
GIL 없는 파이썬은 이 판단 기준을 바꿀까
Python 3.13부터 실험적으로 GIL을 없앤 빌드(PEP 703, "free-threaded" 빌드, python3.13t)가 배포되기 시작했습니다. 이게 자리 잡으면 스레드로도 CPU 바운드 작업을 병렬화할 수 있게 되어 위 표의 첫 줄이 바뀔 수 있습니다. 다만 이 글을 쓰는 시점 기준으로 free-threaded 빌드는 여전히 실험 단계이고, C 확장 생태계(NumPy, Pillow 등 많은 라이브러리가 C로 GIL에 의존해 작성됨) 전체가 이 모드에서 안전하게 동작하도록 재검증되는 데는 시간이 걸릴 것으로 보입니다. 지금 당장 실무 코드의 동시성 전략을 free-threaded 빌드를 전제로 짜는 건 시기상조라고 봅니다 — 표준 빌드를 기준으로 이 장의 판단 기준을 적용하고, free-threaded 빌드는 사용 중인 라이브러리들이 공식 지원을 선언한 뒤에 검토하는 편이 안전합니다.