🔥 sync.Mutex로 동시성 문제 해결하기

344자
5분

채널을 사용하여 고루틴 간의 통신을 할 수 있다는 걸 배웠어요. 그런데 만약 통신이 필요하지 않다면 어떨까요? 단순히 변수에 동시에 접근하는 것을 막아서 충돌을 피하고 싶다면 말이에요.

이런 개념을 *상호 배제(Mutual Exclusion)*라고 하며, 이를 제공하는 데이터 구조를 관례적으로 *뮤텍스(Mutex)*라고 부른답니다.

Go 표준 라이브러리에서는 sync.Mutex와 다음 두 메서드를 통해 상호 배제를 제공해요.

  • Lock
  • Unlock

Inc 메서드에서 보는 것처럼 LockUnlock 호출로 둘러싸인 코드 블록은 상호 배제로 실행된답니다.

Value 메서드처럼 defer를 사용하면 뮤텍스가 잠금 해제되는 것을 보장할 수도 있어요.

package main
 
import (
    "fmt"
    "sync"
    "time"
)
 
// SafeCounter는 동시에 사용해도 안전해요.
type SafeCounter struct {
    mu sync.Mutex
    v  map[string]int
}
 
// Inc는 주어진 키의 카운터를 증가시켜요.
func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    // Lock으로 c.v 맵에는 한 번에 하나의 고루틴만 접근할 수 있어요.
    c.v[key]++
    c.mu.Unlock()
}
 
// Value는 주어진 키에 대한 카운터의 현재 값을 반환해요.
func (c *SafeCounter) Value(key string) int {
    c.mu.Lock()
    // Lock으로 c.v 맵에는 한 번에 하나의 고루틴만 접근할 수 있어요.
    defer c.mu.Unlock()
    return c.v[key]
}
 
func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        go c.Inc("somekey")
    }
 
    time.Sleep(time.Second)
    fmt.Println(c.Value("somekey"))
}
 
go

이 예제에서는 SafeCounter 구조체를 정의했어요. 이 구조체는 sync.Mutex를 포함하고 있으며, 내부적으로 map[string]int 타입의 v를 가지고 있죠.

Inc 메서드는 특정 키의 카운터를 증가시키는 역할을 해요. 이때 c.mu.Lock()으로 먼저 뮤텍스를 잠그고, c.v[key]++로 맵의 값을 증가시킨 후, c.mu.Unlock()으로 뮤텍스를 풀어주는 과정을 거치게 되죠. 이렇게 하면 c.v 맵에는 한 번에 하나의 고루틴만 접근할 수 있게 되면서 경쟁 조건을 피할 수 있답니다.

Value 메서드는 특정 키의 현재 카운터 값을 반환해요. 여기서도 마찬가지로 c.mu.Lock()으로 뮤텍스를 잠그고, defer c.mu.Unlock()을 사용하여 함수가 종료되기 전에 뮤텍스를 풀어주도록 했죠. 이렇게 하면 c.v 맵에 안전하게 접근할 수 있게 된답니다.

main 함수에서는 SafeCounter 인스턴스를 생성하고, 1000개의 고루틴을 생성하여 동시에 Inc 메서드를 호출하고 있어요. 1초 후에 Value 메서드를 호출하여 결과를 출력하는데, 뮤텍스를 사용했기 때문에 경쟁 조건 없이 안전하게 카운터 값을 증가시키고 읽어올 수 있답니다.

이렇게 Go에서는 sync.Mutex를 사용하여 간단하면서도 효과적으로 상호 배제를 구현할 수 있어요. 고루틴 간 통신이 필요하지 않고 단순히 공유 자원에 대한 접근을 제어하고 싶을 때 뮤텍스를 활용해 보세요. 경쟁 조건을 예방하고 안전한 동시성 프로그래밍을 할 수 있을 거예요!