Menu
Coddy logo textTech

sync.Mutex & sync.RWMutex

Coddy GO 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 107개 중 63번째.

채널은 고루틴을 조정하는 Go의 선호되는 방식이지만, 때로는 공유 데이터를 직접 보호해야 할 때가 있습니다. sync 패키지는 뮤텍스(mutexes)를 제공합니다. 이는 한 번에 하나의 고루틴만 리소스에 접근할 수 있도록 보장하는 잠금 장치입니다.

sync.Mutex에는 Lock()Unlock()이라는 두 가지 메서드가 있습니다. 고루틴이 Lock()을 호출하면 독점적인 접근 권한을 얻습니다. Lock()을 호출하는 다른 고루틴들은 Unlock()이 호출될 때까지 대기(block)하게 됩니다:

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

defer c.mu.Unlock()을 사용하면 함수가 조기에 반환되거나 패닉(panic)이 발생하더라도 잠금(lock)이 해제되도록 보장합니다. 이는 일반적이고 권장되는 패턴입니다.

읽기 작업은 빈번하지만 쓰기 작업은 드문 경우, sync.RWMutex가 더 나은 성능을 제공합니다. 여러 읽기 작업이 동시에 수행되는 것을 허용하지만, 쓰기 작업은 독점적인 접근 권한을 갖습니다:

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) string {
    c.mu.RLock()         // 여러 읽기 작업이 허용됨
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()          // 쓰기를 위한 배타적 접근
    defer c.mu.Unlock()
    c.data[key] = value
}

읽기 작업에는 RLock()/RUnlock()을 사용하고, 쓰기 작업에는 Lock()/Unlock()을 사용하세요. 이를 통해 동시 읽기를 허용하면서도 쓰기 작업의 안전을 보장할 수 있습니다.

challenge icon

챌린지

쉬움

제품 재고 수준을 추적하는 스레드 안전(thread-safe) 인벤토리 시스템을 구축해 보겠습니다. 여러분의 시스템은 뮤텍스(mutex)를 사용하여 동시 읽기 및 쓰기를 안전하게 처리하며, 여러 작업이 동시에 발생할 때 데이터 무결성을 보장합니다.

코드는 두 개의 파일로 구성됩니다:

  • inventory.go: 스레드 안전 인벤토리 관리 시스템을 정의합니다.

    맵에 제품 수량을 저장하고 sync.RWMutex를 사용하여 접근을 보호하는 Inventory 구조체를 생성하세요. 인벤토리는 다음 작업들을 지원해야 합니다:

    • NewInventory() *Inventory - 초기화된 맵을 가진 새 인벤토리를 생성합니다.
    • AddStock(product string, quantity int) - 제품의 재고에 수량을 추가합니다 (데이터를 수정하므로 배타적 잠금(exclusive lock)을 사용하세요).
    • GetStock(product string) int - 제품의 현재 재고를 반환하거나, 찾지 못한 경우 0을 반환합니다 (데이터를 읽기만 하므로 읽기 잠금(read lock)을 사용하세요).
    • RemoveStock(product string, quantity int) bool - 충분한 재고가 있는 경우 재고에서 수량을 제거합니다. 성공하면 true를, 재고가 부족하면 false를 반환합니다 (배타적 잠금을 사용하세요).

    잠금이 항상 제대로 해제되도록 defer를 사용하여 언락(unlock)하는 것을 잊지 마세요.

  • main.go: 작업을 읽고 스레드 안전 인벤토리를 시연합니다.

    작업의 수를 읽은 다음, 각 작업을 처리합니다. 각 작업은 유형(add, get, 또는 remove), 제품 이름, 그리고 add/remove 작업의 경우 수량을 가집니다.

    각 작업에 대해 결과를 출력하세요:

    • add: Added [quantity] [product]를 출력합니다.
    • get: [product]: [stock] in stock을 출력합니다.
    • remove: 성공하면 Removed [quantity] [product]를 출력하고, 그렇지 않으면 Insufficient stock for [product]를 출력합니다.

다음 입력이 제공됩니다:

  • 줄 1: 작업의 수 (정수)
  • 다음 줄들: 각 작업에 대해:
    • 작업 유형 (add, get, 또는 remove)
    • 제품 이름
    • 수량 (addremove 작업에만 해당)

예를 들어, 다음과 같이 주어지면:

5
add
apples
50
get
apples
remove
apples
30
remove
apples
25
get
apples

출력은 다음과 같아야 합니다:

Added 50 apples
apples: 50 in stock
Removed 30 apples
Insufficient stock for apples
apples: 20 in stock

RWMutex는 여러 GetStock 호출이 동시에 읽을 수 있도록 허용하는 반면, AddStockRemoveStock은 인벤토리를 수정할 때 배타적 접근 권한을 얻습니다.

치트 시트

sync 패키지는 여러 고루틴이 동일한 리소스에 직접 액세스해야 할 때 공유 데이터를 보호하기 위해 뮤텍스(mutexes)를 제공합니다.

sync.Mutex

sync.MutexLock()Unlock()을 사용하여 배타적 액세스를 제공합니다:

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

Unlock()과 함께 defer를 사용하면 함수가 조기에 반환되거나 패닉이 발생하더라도 잠금이 해제되도록 보장합니다.

sync.RWMutex

sync.RWMutex는 여러 명의 동시 읽기 작업자(reader)를 허용하지만 쓰기 작업자(writer)에게는 배타적 액세스를 제공합니다:

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) string {
    c.mu.RLock()         // 여러 읽기 작업자 허용
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()          // 쓰기 작업을 위한 배타적 액세스
    defer c.mu.Unlock()
    c.data[key] = value
}

읽기 작업에는 RLock()/RUnlock()을 사용하고 쓰기 작업에는 Lock()/Unlock()을 사용하세요.

직접 해보기

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	
	// 연산 횟수 읽기
	line, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(line))
	
	// 새 인벤토리 생성
	inventory := NewInventory()
	
	// 각 연산 처리
	for i := 0; i < numOps; i++ {
		// 연산 유형 읽기
		opLine, _ := reader.ReadString('\n')
		opType := strings.TrimSpace(opLine)
		
		// 제품 이름 읽기
		productLine, _ := reader.ReadString('\n')
		product := strings.TrimSpace(productLine)
		
		// TODO: 각 연산 유형 처리 (add, get, remove)
		// "add" 및 "remove"의 경우, 입력에서 수량을 읽음
		// 적절한 인벤토리 메서드 호출
		// 챌린지 설명에 따라 결과 출력
		
		switch opType {
		case "add":
			// TODO: 수량을 읽고, 재고를 추가하고, 결과를 출력함
			
		case "get":
			// TODO: 재고를 가져오고, 결과를 출력함
			
		case "remove":
			// TODO: 수량을 읽고, 재고를 제거하고, 적절한 결과를 출력함
			
		}
	}
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨