Go의 동시 컬렉션에 대한 작업은 다중 스레드 애플리케이션에 대한 증가하는 요구 때문에 중요한 주제가 되었습니다. Go의 일반 map은 스레드 안전하지 않으며 데이터 레이스(data race)를 초래할 수 있습니다. sync.Map의 존재는 외부 동기화 없이 안전하게 컬렉션에 공동 접근하는 표준 솔루션을 제공했습니다.
문제의 역사:
sync.Map이 등장하기 전에는 개발자들이 여러 고루틴에서 안전하게 접근하기 위해 일반 map과 외부 Mutex 또는 RWMutex를 사용해야 했습니다. 이는 코드의 양을 증가시키고 동기화 오류의 가능성을 높였습니다. Go 1.9에서는 동시 컬렉션 작업을 단순화하기 위해 sync.Map이 도입되었습니다.
문제:
일반 map은 스레드 안전하지 않습니다. 여러 고루틴이 동기화 없이 map을 읽고 쓰면 패닉(panic)이나 예기치 않은 결과를 초래할 수 있습니다. Mutex는 올바르게 사용하는 것이 어려워 블로킹(blocking) 및 성능 저하를 초래할 수 있습니다. 또한 "이중 검사(double check)" 및 복잡한 동기화 작업과 관련된 어려움이 발생합니다.
해결책:
sync.Map은 표준 라이브러리에 있는 특별한 구조체로, 스레드 안전한 Load, Store, LoadOrStore, Delete, Range 메서드를 제공합니다. 이것은 부분적으로 lock-free 전략을 구현하며, 자주 읽고 드물게 쓰는 시나리오에 최적화되어 있습니다.
코드 예:
import ( "fmt" "sync" ) func main() { var m sync.Map m.Store("foo", 42) value, ok := m.Load("foo") fmt.Println(value, ok) // 42 true m.Delete("foo") }
주요 특징:
모든 map을 동시 프로그램에서 sync.Map으로 대체할 수 있나요?
아니요, sync.Map은 일반 map의 범용적인 대체물이 아닙니다. 이는 경쟁적이고 독립적인 읽기가 우세한 데이터 구조에 잘 맞지만, 빈번한 수정이나 소규모 컬렉션에서는 일반 map + Mutex가 더 빠르고 효율적입니다.
일반 map을 여러 고루틴에서 읽기 위해서만 사용한다면 어떤 일이 일어날까요?
map이 완전히 초기화되고 모든 고루틴 실행 후 변경되지 않는다면 병렬 읽기는 허용되며 안전합니다. 그러나 데이터를 삭제하거나 변경하면 예측할 수 없는 동작, 패닉, corrupted map이 발생할 수 있습니다.
sync.Map의 키로 어떤 데이터 타입을 사용할 수 있나요?
규칙은 일반 map과 동일합니다: 오직 비교 가능한 타입(comparable types)만 사용 가능합니다. 그러나 sync.Map은 키로 인터페이스{}의 어떤 타입도 받을 수 있어 서로 비교할 수 없는 서로 다른 의미론을 가진 객체를 생성하거나 런타임 오류의 가능성을 높일 수 있습니다.
코드 예:
var m sync.Map m.Store([]int{1,2}, "value") // panic: runtime error: hash of unhashable type []int
개발자가 일반적으로 변경이 드문 애플리케이션 설정을 저장하기 위해 sync.Map을 사용했습니다. 그러나 이후 사용자의 세션 데이터가 대량으로 기록되기 시작하여 GC의 부하가 예상치 않게 증가하고 성능이 저하되었습니다.
장점:
단점:
팀이 고부하 서비스에서 자주 요청되는 계산 결과의 캐시를 저장하기 위해 sync.Map을 도입했습니다. "읽기"의 수가 "쓰기"보다 수백 배 많습니다. 모든 것이 안정적이고 효율적으로 작동하며 코드는 더 짧고 유지 관리가 쉬워졌습니다.
장점:
단점: