전체 프로그램 중에서 병렬화할 수 없는 부분이 전체 성능 향상의 한계를 결정한다는 법칙이야.
즉, 프로그램의 일부만 병렬로 빠르게 처리해도, 나머지 병렬화 불가능한 구간이 발목을 잡는다는 의미지.
Speedup(N) = 1 / [ (1 - P) + (P / N) ]
| 기호 | 의미 |
|---|---|
| N | 사용한 프로세서(코어) 수 |
| P | 전체 프로그램 중 병렬화 가능한 비율 (0 ≤ P ≤ 1) |
| 1 - P | 병렬화 불가능한 부분 |
| Speedup(N) | 병렬화 후의 성능 향상 배수 |
병렬화 가능한 비율 P가 아무리 높더라도, 병렬화 불가능한 부분이 존재하면 무한히 많은 코어를 써도 속도 향상에 한계가 있어.
여기서 (1 - P)는 병렬화가 안 되는 부분 → 항상 한 코어에서만 실행됨.
즉, 여기는 병목 구간이야.
코어 수 N이 무한대로 갈 경우:
lim N→∞ Speedup(N) = 1 / (1 - P)
→ 병렬화가 95%만 가능해도, 최대 성능 향상은 1 / (1 - 0.95) = 20배가 한계
| 코어 수 (N) | Speedup |
|---|---|
| 1 | 1.00 |
| 2 | 1.66 |
| 4 | 2.50 |
| 8 | 3.33 |
| ∞ | 5.00 |
→ 병렬화 비율이 높지 않으면, 코어를 늘려도 성능이 생각보다 많이 안 오름
P = 0.5 → 병렬화 비율 50%
코어 수 N = 1,000
Speedup = 1 / [0.5 + (0.5 / 1000)] ≈ 1.998
→ 1000개 코어를 써도 고작 2배 빨라짐이게 바로 병렬화가 코어 수보다 중요한 이유야.
P = 0.99 (99% 병렬화)
N = 8
Speedup ≈ 1 / (0.01 + 0.99/8) ≈ 7.48배
→ 단 8코어로도 거의 이상적인 8배 가까운 성능 향상즉, 병렬화가 잘 돼 있으면, 코어 수는 적어도 큰 효과를 낼 수 있음
🔍 어디서?
웹 서버 (NGINX, Apache)
Node.js, Java Spring 기반 애플리케이션
대규모 멀티스레딩 구조
📌 어떻게 활용?
서버 요청 처리 로직에서 병렬화 가능한 영역(예: 요청 파싱, DB 접근 등)과 불가능한 영역(로깅, 공용 리소스 접근 등)을 분석
병렬 처리 설계 전/후의 성능 추정과 실제 속도 향상의 차이 분석에 활용
🎯 실제 영향
병렬화된 워커 수 늘리기 전에, 스레드 안전성과 공유 리소스 병목 제거를 먼저 시도
“코어 수 늘리기 전에 구조 먼저 바꿔야 한다”는 결론을 도출
| 항목 | 직렬 처리 (Sequential) | 병렬 처리 (Parallel) |
|---|---|---|
| 실행 방식 | 작업을 순서대로 하나씩 처리 | 작업을 여러 개 동시에 처리 |
| 실행 흐름 | 하나의 흐름 (Single thread/process) | 다중 흐름 (Multi-threaded or multi-core) |
| 하드웨어 활용 | 보통 1개 코어 사용 | 여러 코어/CPU/GPU 사용 |
| 성능 | 느리지만 단순하고 예측 가능 | 빠르지만 동기화/경쟁 상태 관리 필요 |
| 예시 | 파일 읽기 → 처리 → 저장 | 이미지 여러 장 필터링, 동영상 렌더링 등 |
| 측면 | 설명 |
|---|---|
| 속도 | 병렬 처리 → 시간 단축 효과 큼 (특히 대용량 작업에 유리) |
| 복잡성 | 병렬 처리에는 스레드 관리, 동기화, 공유 리소스 관리 필요 |
| 정확성 | 직렬 처리는 순차적이므로 동기화 문제 없음 |
| 확장성 | 병렬 처리는 멀티코어, 클러스터, GPU 등으로 확장 가능 |
● 직렬 처리
1. 세탁기 돌림 → 끝날 때까지 기다림
2. 빨래 널기
3. 다림질
→ 한 작업이 끝나야 다음으로 넘어가니까 전체 시간 소요가 큼
● 병렬 처리
1. 세탁기 돌리는 동안 → 다른 세탁기에서 두 번째 빨래
2. 첫 세탁 끝나면 널기 + 두 번째 세탁 기다리는 동안 다림질 시작
→ 여러 작업을 동시에 수행 → 전체 시간 대폭 단축
● 직렬 처리
for image in image_list:
resize(image) # 한 번에 하나씩 처리
→ 총 100개의 작업을 하나하나 순서대로 처리→ 총 걸린 시간 = 이미지 1개 처리 시간 × 100
● 병렬 처리
with ThreadPoolExecutor(max_workers=8) as executor:
executor.map(resize, image_list) # 동시에 8개씩 처리
→ 최대 8개씩 동시에 리사이징→ 전체 시간은 거의 1/8 수준으로 단축 가능
● 직렬 처리
1. 밥 짓기 (30분)
2. 국 끓이기 (20분)
3. 반찬 만들기 (15분)
→ 총 65분 소요
● 병렬 처리 (요리사 3명)
- 요리사 1: 밥
- 요리사 2: 국
- 요리사 3: 반찬
→ 총 30분이면 전부 완료
→ 각각의 작업이 서로 독립적이고, 동시에 수행 가능 → 병렬 처리
| 구분 | 의미 |
|---|---|
| 동기(Synchronous) | 결과가 나올 때까지 기다림, 다음 작업 못 함 |
| 비동기(Asynchronous) | 기다리지 않고 바로 다음 작업 수행, 결과는 나중에 처리 |
| 직렬(Sequential) | 작업을 하나씩 순서대로 처리 |
| 병렬(Parallel) | 여러 작업을 동시에 처리 (멀티코어, 멀티스레드 등) |
| 처리 방식 | 동기 | 비동기 |
|---|---|---|
| 직렬 | 순차적으로 기다림 | 순차적으로 호출하되 기다리진 않음 |
| 병렬 | 동시에 실행되지만 결과 기다림 | 동시에 실행 + 결과 기다리지 않음 |
→ 헷갈리지 않게 하기 위해, 같은 예시를 4가지 방식으로 비교해볼게.
물 끓이기 → 면 넣기 → 다 익을 때까지 기다리기 → 다음 그릇 시작
for i in range(3):
boil_water()
cook_noodles()
serve()
세 개의 라면 끓이기 시작만 하고, 끝날 때까지 기다리진 않음, 대신 타이머 등록해두고 하나씩 처리
for i in range(3):
async_start_boiling(i) # 바로 다음 라면 시도
3개의 냄비로 라면을 동시에 끓이되, 각 냄비가 다 익을 때까지 기다림
threads = []
for i in range(3):
t = Thread(target=sync_cook)
t.start()
threads.append(t)
for t in threads:
t.join() # 결과 기다림
세 냄비에 동시에 끓이고, 다 익었는지 확인은 타이머/콜백으로 나중에 처리
async def main():
tasks = [async_cook(i) for i in range(3)]
await asyncio.gather(*tasks)