sync/atomic package에는 하나 이상의 goroutine에서 데이터 동기화에 사용할 수 있는 high-performance의 type을 제공하고 있다. 그중에서 go 1.19부터 추가된 generic type인 atomic.pointer에 대하여 알아보자.
아래의 TestRaceCondition를 "go test -race"로 실행 시키면 Race Condtion(mutual exclusion)이 발생한다.
type Race struct {
Value string
}
func TestRaceCondition(t *testing.T) {
c := make(chan bool)
race := &Race{
Value: "init",
}
go func() {
race.Value = "first" // First conflicting access.
c <- true
}()
race.Value = "second" // Second conflicting access.
<-c
t.Log("race value", race.Value)
}

Mutual exclusion을 해결하기 위해서 atomic.Pointer를 사용하면, race codition이 발생하지 않는 결과를 확인할 수 있다.
func TestRaceConditionAtomicPointer(t *testing.T) {
c := make(chan bool)
race := atomic.Pointer[Race]{}
race.Store(&Race{
Value: "init",
})
go func() {
race.Swap(&Race{
Value: "first",
})
c <- true
}()
{
race.Swap(&Race{
Value: "second",
})
}
<-c
t.Log("race value", *race.Load())
}

sync.mutex를 사용할 때보다 atomic.Pointer를 사용하는 것이 실제로 performence가 향상되는지를 테스트 해 보자.
$ go test -bench . -count 2
func BenchmarkRaceCondition(b *testing.B) {
mu := sync.Mutex{}
race := &Race{
Value: "init",
}
b.ResetTimer()
b.StartTimer()
for i := 0; i < b.N; i++ {
go func() {
mu.Lock()
race = &Race{
Value: "first",
}
mu.Unlock()
}()
mu.Lock()
race = &Race{
Value: "scond",
}
mu.Unlock()
}
b.StopTimer()
b.Log("race value", race.Value)
}
func BenchmarkConditionAtomicPointer(b *testing.B) {
race := atomic.Pointer[Race]{}
race.Store(&Race{
Value: "init",
})
b.ResetTimer()
b.StartTimer()
for i := 0; i < b.N; i++ {
go func() {
race.Swap(&Race{
Value: "first",
})
}()
{
race.Swap(&Race{
Value: "second",
})
}
}
b.StopTimer()
b.Log("race value", *race.Load())
}
benchmark결과에서 atomic.Pointer(BenchmarkConditionAtomicPointer)를 사용하는 것이 약100n/s 빠르다. (일반적으로 atomic은 mutex를 사용하는 것보다 성능이 좋다)

database의 connection을 13초마다 새롭게 생성하고, 다른 gorutine에서는 11초마다 connection 정보를 출력한다면 아래와 같이 사용 가능하다. 하지만 11초와 13초는 동시에 실행되지 않을 수도 있다(이런 경우는 atomic.pointers는 over-engineering이다). 내가 실행하고 하는 코드가 over-engineering인지 확인하기 위해서는 atomic.Pointer 대신 일반 Pointer를 사용하여 코드를 작성하고 -race 옵션으로 실행한다면 판단할 수 있다.
type ServerConn struct {
Connection net.Conn
ID string
Open bool
}
func ShowConnection(p *atomic.Pointer[ServerConn]) {
for {
time.Sleep(11 * time.Second)
fmt.Println(p, p.Load())
}
}
func main() {
c := make(chan bool)
p := atomic.Pointer[ServerConn]{}
s := ServerConn{ID: "first_conn"}
p.Store(&s)
go ShowConnection(&p)
go func() {
for {
time.Sleep(13 * time.Second)
newConn := ServerConn{ID: "new_conn"}
p.Swap(&newConn)
}
}()
<-c
}
잘 보고 갑니다!