빠른 비교
| 표면 | 역할 |
|---|---|
WaitGroup | 여러 goroutine이 끝날 때까지 기다림 |
Mutex | 공유 상태를 한 번에 하나만 접근하게 보호 |
RWMutex | 읽기는 여러 개, 쓰기는 하나로 제한 |
atomic | 아주 작은 값의 원자적 읽기/쓰기 |
| channel | 값 전달과 소유권 이동 |
Go 동시성은 channel만으로 끝나지 않습니다. 작업 종료 대기와 공유 상태 보호는 sync 패키지로 명확히 표현할 때가 많습니다.
기본 흐름
WaitGroup은 종료 대기용이다
var wg sync.WaitGroup
for _, job := range jobs {
wg.Add(1)
go func(job Job) {
defer wg.Done()
process(job)
}(job)
}
wg.Wait()WaitGroup은 결과 전달 도구가 아니라 goroutine 종료를 기다리는 도구입니다. 에러나 결과는 channel, mutex로 보호한 변수, 별도 그룹 패턴으로 전달합니다.
Mutex는 공유 상태를 보호한다
var mu sync.Mutex
counts := map[string]int{}
mu.Lock()
counts[key]++
mu.Unlock()여러 goroutine이 같은 map이나 struct를 동시에 바꾸면 data race가 생길 수 있습니다. 공유 상태는 mutex로 보호하거나, channel로 소유권을 한 goroutine에 모읍니다.
defer Unlock으로 해제를 보장한다
mu.Lock()
defer mu.Unlock()
counts[key]++함수 안에서 lock을 잡는 경우 defer로 unlock을 보장하면 중간 return이나 panic 흐름에서도 해제가 빠질 가능성이 줄어듭니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| goroutine 종료만 기다림 | sync.WaitGroup |
| map/struct 공유 수정 | sync.Mutex |
| 읽기가 훨씬 많고 쓰기가 적음 | sync.RWMutex 검토 |
| counter나 flag 같은 작은 값 | sync/atomic 검토 |
| 작업을 전달하고 소유권을 넘김 | channel |
| 취소 전파 | context.Context |
동시성 설계는 "channel 또는 mutex"의 취향 문제가 아니라, 값의 소유권이 이동하는지 공유 상태를 보호하는지의 문제입니다.
주의할 점
WaitGroup.Add는 goroutine 안이 아니라 시작 전에 호출하는 편이 안전합니다. Mutex를 복사하거나 lock/unlock 경로가 어긋나면 찾기 어려운 버그가 됩니다. data race는 테스트에서 항상 터지지 않으므로 공유 상태가 있으면 go test -race로 확인하는 습관이 중요합니다.
참고 링크
3 sources