“Concurrency is not parallelism” — Rob Pike
Go Proverbs의 “Concurrency is not parallelism"은 동시성과 병렬성이 같은 개념이 아니라는 사실을 이야기한다.
동시성은 작업을 독립적으로 진행할 수 있도록 구조를 나누는 것이고, 병렬성은 나누어진 작업을 여러 코어에서 실제로 동시에 실행하는 것이다.
두 개념은 서로 연결되어 있지만 동일하지 않다.
동시성은 구조이고, 병렬성은 실행이다
“Concurrency is about structure, parallelism is about execution.” (동시성은 구조에 대한 이야기이고, 병렬성은 실행에 대한 이야기다.)
— Rob Pike, Concurrency is not Parallelism (Waza, 2012)
동시성은 여러 작업을 독립적으로 수행할 수 있도록 프로그램을 구성하는 방법이다. 병렬성은 그렇게 나누어진 작업들이 여러 CPU 코어에서 실제로 동시에 실행되는 상태를 의미한다. 중요한 점은 동시성은 코드만 봐도 알 수 있지만, 병렬성은 실행 환경을 알아야 판단할 수 있다는 것이다.
고루틴을 100개 만드는 코드는 어디에서 실행하든 동시적인 코드다. 하지만 그 100개의 고루틴이 실제로 몇 개씩 동시에 실행되는지는 CPU 코어 수와 GOMAXPROCS 설정에 따라 달라진다.
기다리는 작업은 동시성만으로도 빨라진다
대부분의 시간이 I/O 대기인 작업이라면 병렬성이 없어도 큰 성능 향상을 얻을 수 있다.
// 순차 실행
for i := 0; i < 100; i++ {
time.Sleep(10 * time.Millisecond)
}
// 동시 실행
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(10 * time.Millisecond)
}()
}
wg.Wait()
순차 버전은 기다림이 끝나야 다음 작업을 시작하지만, 동시 실행 버전은 모든 작업이 동시에 기다리기 시작한다.
GOMAXPROCS=1처럼 코어를 하나만 사용해도 실행 시간은 크게 줄어든다.
동시성이 성능을 높이는 이유는 CPU를 더 많이 쓰는 것이 아니라 놀고 있는 시간을 줄이기 때문이다.
계산 작업은 병렬성이 필요하다
CPU 계산이 대부분인 작업은 이야기가 다르다.
for _, job := range jobs {
process(job)
}
위 코드를 아래 처럼 워커 풀로 바꿔도 GOMAXPROCS=1이라면 실행 시간은 거의 변하지 않는다.
해야 하는 계산량 자체는 그대로이기 때문이다.
하지만 GOMAXPROCS를 늘려 여러 코어에서 동시에 실행하면 처리 시간이 점차 감소한다.
for i := 0; i < workers; i++ {
go worker(jobCh)
}
즉, 작업을 나누는 것은 동시성이고, 나누어진 작업을 여러 코어에서 실행하는 것은 병렬성이다.
Go는 동시성과 병렬성을 어떻게 나눠 놓았는가
Go에서 고루틴과 채널은 동시성을 표현하는 문법이고, GOMAXPROCS는 병렬성을 정하는 설정이다. 언어 문법에는 병렬성을 지정하는 수단이 아예 없다.
Go 스케줄러의 G-M-P 모델에 이 분리가 그대로 들어 있다.
| 요소 | 무엇인가 | 어느 쪽 개념인가 |
|---|---|---|
| G (goroutine) | 따로 진행할 수 있는 작업 하나 | 동시성 — 수십만 개도 만들 수 있다 |
| P (processor) | Go 코드를 실행할 수 있는 자리 | 병렬성 — 자리 수가 곧 GOMAXPROCS |
| M (machine) | 실제 OS 스레드 | 자리에 앉아 고루틴을 실행하는 주체 |
고루틴은 얼마든지 만들 수 있지만, 어느 순간에 실제로 Go 코드를 실행 중인 고루틴 수는 자리 수를 넘을 수 없다. 고루틴을 10만 개 띄워도 GOMAXPROCS가 2면 같은 순간에 달리는 것은 2개다.
한 가지 자주 오해되는 지점이 있다. GOMAXPROCS는 Go 코드를 동시에 실행하는 자리 수의 상한이지, 프로그램이 만드는 OS 스레드 수의 상한이 아니다. 파일 읽기 같은 시스템 콜에서 멈춘 고루틴은 자기 자리를 다른 스레드에 넘겨주고 물러나므로, GOMAXPROCS=1인 프로그램도 스레드는 여러 개를 가질 수 있다.
맺음말
동시성은 작업을 어떻게 나눌 것인가에 대한 답이고, 병렬성은 그 작업을 몇 명이 처리할 것인가에 대한 답이다.
References
- Rob Pike, “Concurrency is not Parallelism” (Heroku Waza, 2012)
- Andrew Gerrand, “Concurrency is not parallelism” (The Go Blog, 2013)
- Rob Pike, “Go Proverbs” (Gopherfest, 2015)
- Edsger W. Dijkstra, “Cooperating Sequential Processes” (EWD123, 1965)
- Go 표준 라이브러리,
runtime.GOMAXPROCS문서 (Go 1.26.5) - The Go Memory Model
- automaxprocs
