• Feed
  • Explore
  • Ranking
/
/
    Go

    Go sync 패키지에 대하여

    Go sync package
    GoDevOps
    A
    Antonio
    2025.03.10
    ·
    12 min read

    3861

    개요

    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)

      1. sync.Map의 read 필드는 atomic.Value 타입이므로, 락 없이 atomic한 방식으로 읽기 가능

      2. read.Load(key)를 호출하여 값이 존재하는지 확인

      3. 존재하면 즉시 반환 (O(1) 연산)

      4. 존재하지 않으면 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)

      1. read 맵은 atomic한 읽기 전용이므로, 쓰기 연산은 dirty 맵에서 수행

      2. dirty 맵에 key-value를 추가하면서, 필요 시 sync.Mutex로 보호

      3. 일정 조건이 충족되면 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 방식

      1. 삭제 요청이 들어오면 dirty 맵에서 키를 제거

      2. read 맵에는 삭제 요청을 반영하지 않고, 기존 데이터를 유지 (즉시 제거 X)

      3. 이후 일정 조건이 충족되면 read 맵을 dirty 맵으로 교체하며, 삭제된 키를 반영

    func (m *Map) Delete(key interface{}) {
        m.mu.Lock()
        defer m.mu.Unlock()
        delete(m.dirty, key)
    }
    

    기능

    sync.Map

    일반 map

    동시성 지원

    ✅ Yes

    ❌ No
    (map은 동시 접근 시 fatal error: concurrent map writes 발생)

    읽기 성능

    ✅ 빠름 (atomic.Value 사용)

    🔹 보통 (락 필요 없음)

    쓰기 성능

    ⚠ 보통 (dirty 맵과 sync.Mutex 사용)

    ✅ 빠름

    메모리 사용

    ⚠ 높음 (두 개의 맵 사용)

    ✅ 낮음

    삭제 방식

    Lazy deletion

    즉시 삭제


    sync package - sync - Go Packages
    https://pkg.go.dev/sync






    - 컬렉션 아티클