
개요
Go의 sync 패키지는 고루틴 간 동시성 제어를 위해 사용됩니다. 주로 멀티 쓰레드 환경에서 데이터 경쟁을 방지하고 성능을 최적화하는 데 사용됩니다.
sync.Mutex (뮤텍스 - Mutual Exclusion)
sync.Mutex는 한 번에 하나의 고루틴만 특정 코드 블록을 실행할 수 있도록 하는 상호 배제(뮤텍스, mutual exclusion) 락입니다.
package main
import (
"fmt"
"sync"
)
var (
counter int
mutex sync.Mutex
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mutex.Lock() // 락 획득
counter++ // 공유 데이터 변경
mutex.Unlock() // 락 해제
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Final Counter:", counter) // 10이 보장됨
}
mutex.Lock()→ 한 번에 하나의 고루틴만counter++실행 가능mutex.Unlock()→ 다른 고루틴이counter++실행할 수 있도록 해제데이터 경쟁 조건 없이 안전하게 증가 (
race condition방지)
sync.RWMutex (읽기/쓰기 분리 락)
sync.Mutex와 유사하지만 읽기(Read) 연산을 동시에 여러 고루틴이 수행할 수 있도록 허용합니다.쓰기(Write) 연산이 실행될 때는 다른 모든 연산이 차단됩니다.
package main
import (
"fmt"
"sync"
"time"
)
var (
rwMutex sync.RWMutex
data int
)
func readData(id int, wg *sync.WaitGroup) {
defer wg.Done()
rwMutex.RLock() // 읽기 락 (여러 고루틴이 동시에 실행 가능)
fmt.Printf("Reader %d: Read data = %d\\n", id, data)
time.Sleep(time.Millisecond * 100)
rwMutex.RUnlock()
}
func writeData(wg *sync.WaitGroup) {
defer wg.Done()
rwMutex.Lock() // 쓰기 락 (다른 모든 고루틴이 차단됨)
data++
fmt.Println("Writer: Updated data =", data)
time.Sleep(time.Millisecond * 200)
rwMutex.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go readData(i, &wg)
}
wg.Add(1)
go writeData(&wg)
wg.Wait()
}
여러 개의 읽기 고루틴(
RLock())이 동시에 실행 가능쓰기 고루틴(
Lock())이 실행되면 모든 읽기/쓰기 연산이 차단sync.RWMutex는 읽기 연산이 많고, 쓰기 연산이 적은 경우에 성능 최적화를 위해 사용합니다.
sync.WaitGroup (고루틴 완료 대기)
sync.WaitGroup은 고루틴이 종료될 때까지 기다리는 역할을 합니다.
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 작업 완료 시 WaitGroup 카운트 감소
fmt.Printf("Worker %d starting\\n", id)
time.Sleep(time.Second) // 작업 수행
fmt.Printf("Worker %d done\\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // WaitGroup 카운트 증가
go worker(i, &wg)
}
wg.Wait() // 모든 고루틴이 완료될 때까지 대기
fmt.Println("All workers done")
}
wg.Add(1)→ 새로운 고루틴을 시작할 때 호출 (WaitGroup 카운트 증가)wg.Done()→ 고루틴이 끝날 때 호출 (WaitGroup 카운트 감소)wg.Wait()→ 카운트가 0이 될 때까지 대기
sync.Once (한 번만 실행)
package main
import (
"fmt"
"sync"
)
var once sync.Once
func initialize() {
fmt.Println("Initialization done!")
}
func worker(wg *sync.WaitGroup) {
defer wg.Done()
once.Do(initialize) // 모든 고루틴 중 단 한 번만 실행됨
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go worker(&wg)
}
wg.Wait()
}
once.Do(initialize)는 여러 고루틴에서 호출되더라도 최초 한 번만 실행싱글톤 패턴 또는 초기화 작업에 유용함
sync.Cond (조건 변수)
sync.Cond는 특정 조건이 충족될 때까지 고루틴을 대기(wait)시키고, 조건이 충족되면 신호를 보내(wake) 실행하게 만듭니다.
package main
import (
"fmt"
"sync"
"time"
)
var (
ready bool
cond = sync.NewCond(&sync.Mutex{}) // 조건 변수 생성
)
func waitForCondition(id int, wg *sync.WaitGroup) {
defer wg.Done()
cond.L.Lock()
for !ready {
cond.Wait() // ready가 true가 될 때까지 대기
}
fmt.Printf("Worker %d proceeding\\n", id)
cond.L.Unlock()
}
func setCondition() {
time.Sleep(time.Second)
cond.L.Lock()
ready = true
cond.L.Unlock()
cond.Broadcast() // 모든 대기 중인 고루틴 깨우기
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go waitForCondition(i, &wg)
}
go setCondition()
wg.Wait()
}
cond.Wait()는ready가true가 될 때까지 고루틴을 대기 상태로 만듦cond.Broadcast()는 대기 중인 모든 고루틴을 깨움
sync.Map (동시성 안전한 맵)
Go의 기본 map은 고루틴 간 동시 접근이 안전하지 않습니다. 이에 sync.Map을 사용하여 안전하게 동시 읽기/쓰기를 수행할 수 있습니다.
package main
import (
"fmt"
"sync"
)
var cache sync.Map
func main() {
cache.Store("key1", "value1")
cache.Store("key2", "value2")
value, ok := cache.Load("key1")
if ok {
fmt.Println("Found:", value)
}
cache.Range(func(key, value interface{}) bool {
fmt.Println("Key:", key, "Value:", value)
return true
})
}
sync.Map은 동기화된Load,Store,Delete,Range기능 제공
sync.Map이 동시성을 안전하게 제어하는 원리
sync.Map은 고루틴 간 안전한 동시 읽기/쓰기(Map operations)를 제공하는 구조체입니다. 하지만 내부적으로 단순한 sync.Mutex 기반이 아닌 특수한 최적화 기법을 사용하여 성능을 개선합니다. 즉, sync.Map은 일반적인 map과 다르게 락(lock) 기반이 아니라 읽기/쓰기 패턴에 따라 최적화된 동기화 메커니즘을 사용합니다.
동시성 제어 방법
읽기(Read)와 쓰기(Write)를 분리하여 최적화 (읽기 성능 극대화)
빠른 읽기(Fast-path read) → atomic 연산 사용 (락 없이 동작)
쓰기(Slow-path write) →
sync.Mutex를 사용하여 동기화쓰기 충돌이 발생하면 기존 맵을 새 맵으로 교체하는 Copy-on-Write 전략 적용
오래된 데이터를 자동으로 삭제하여 메모리 누수 방지 (lazy deletion)
sync.Map 내부 구조
type Map struct {
mu sync.Mutex
read atomic.Value // 빠른 읽기(Read path)
dirty map[interface{}]*entry // 느린 읽기 + 쓰기(Write path)
}
sync.Map의 내부는 두 개의 맵으로 이루어져 있습니다.
mu(
sync.Mutex):dirty맵을 보호하는 락read(
atomic.Value): 빠른 읽기 가능 (락 없음)dirty(일반
map[interface{}]*entry): 쓰기 가능하지만sync.Mutex필요
sync.Map의 읽기(Read) 과정
sync.Map은 대부분의 읽기 연산을 빠르게 처리하기 위해 락을 사용하지 않습니다.
빠른 읽기(Fast-path read)
sync.Map의read필드는atomic.Value타입이므로, 락 없이 atomic한 방식으로 읽기 가능read.Load(key)를 호출하여 값이 존재하는지 확인존재하면 즉시 반환 (O(1) 연산)
존재하지 않으면
dirty맵을 확인 (느린 경로로 이동)
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(map[interface{}]*entry)
if e, ok := read[key]; ok {
return e.load(), true
}
return m.loadSlow(key) // dirty map 확인
}
sync.Map의 쓰기(Write) 과정
쓰기 연산은 dirty 맵에서 이루어지며, sync.Mutex로 보호됩니다.
쓰기(Slow-path write)
read맵은 atomic한 읽기 전용이므로, 쓰기 연산은dirty맵에서 수행dirty맵에 key-value를 추가하면서, 필요 시sync.Mutex로 보호일정 조건이 충족되면
read맵을dirty맵으로 교체 (Copy-on-Write)
func (m *Map) Store(key, value interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
if m.dirty == nil {
m.dirty = make(map[interface{}]*entry)
}
m.dirty[key] = newEntry(value)
}
Copy-on-Write 기법
일정 조건이 충족되면
dirty맵을read맵으로 이동이후 읽기 성능을 극대화하기 위해 다시 atomic 읽기가 가능해짐
쓰기 연산이 많지 않다면 대부분
read맵에서 처리되므로 성능이 뛰어남쓰기 연산이 발생하면
sync.Mutex를 사용하여 동기화
sync.Map의 삭제(Delete) 과정
삭제 연산은 데이터를 즉시 제거하는 것이 아니라, 느리게(Lazy deletion) 제거하여 성능을 유지합니다.
Lazy deletion 방식
삭제 요청이 들어오면
dirty맵에서 키를 제거read맵에는 삭제 요청을 반영하지 않고, 기존 데이터를 유지 (즉시 제거 X)이후 일정 조건이 충족되면
read맵을dirty맵으로 교체하며, 삭제된 키를 반영
func (m *Map) Delete(key interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.dirty, key)
}
기능 |
| 일반 |
동시성 지원 | ✅ Yes | ❌ No |
읽기 성능 | ✅ 빠름 ( | 🔹 보통 (락 필요 없음) |
쓰기 성능 | ⚠ 보통 ( | ✅ 빠름 |
메모리 사용 | ⚠ 높음 (두 개의 맵 사용) | ✅ 낮음 |
삭제 방식 | Lazy deletion | 즉시 삭제 |