Golang은 map에 대한 atomic 연산을 지원하지 않는다.

Golang은 map에 대한 atomic 연산을 지원하지 않는다.

Go 의 map 은 여러 고루틴이 동시에 update 할 수 없다.

Go 의 map 에 대한 아토믹 연산이 정의되지 않은 이유

사실 여러 고루틴이 맵에 접근하는 연산을 하게 되는 경우는 이미 동기화된 대규모 데이터 구조나 계산의 일부일 가능성이 크다. 즉 여러 고루틴이 하나의 맵에 안전하게 접근할 필요성이 그다지 크지 않기 때문에 Go의 개발자들은 map에 대한 아토믹 연산을 정의하지 않았다. 어차피 map을 동시에 쓰게되는 상황은 엄청 큰 대규모 연산의 일부일텐데, 이 때 만약 map을 안전하게 사용하기 위해 mutex 등을 강제한다면 성능에 큰 악영향을 끼칠것이기 때문이기도 하다.

거기서 더 나아가서 아토믹하지 않게 map을 업데이트 하게되면 ....?

어차피 Go 언어에서 integer에 대한 접근도 atomic 하지 않다. integer도 동시에 여러 고루틴에서 업데이트 하면 통제되지 않은 결과를 확인하게 된다. 하지만 integer와 다르게, Go언어 자체에서 동시 실행으로 인해 맵이 안전하지 않게 접근되는 경우 런타임에서 이를 에러로 취급하여 보고하게 된다.



아래의 코드를 살펴보면 여러 고루틴이 하나의 map에 접근 하여 동시에 value를 수정하는 작업을 하는 함수 testWriteWithMap()는 실행시 에러를 발생시킨다. 반면에 여러 고루틴이 하나의 맵의 value를 읽는 함수testReadWithMap()는 에러를 발생시키지 않는다. 읽기만 하면 사실 여러 고루틴이 동시에 접근한다고 해서 통제되지 않은 값을 읽게되는일은 없으니 안전하다고 볼 수 있기 때문이다.



Map access is unsafe only when updates are occurring. As long as all goroutines are only reading—looking up elements in the map, including iterating through it using a for range loop—and not changing the map by assigning to elements or doing deletions, it is safe for them to access the map concurrently without synchronization.

As an aid to correct map use, some implementations of the language contain a special check that automatically reports at run time when a map is modified unsafely by concurrent execution.



아래의 코드에서 testWithInt()는 실행시 에러를 발생시키지 않는다. 하지만 원자적 연산을 수행하지 않은 코드이기 때문에 기대한 결과를 얻을 수는 없다. 이 때 에러가 발생하지 않는 이유는 아마 Integer에 대해서는 atomic 패키지를 통해 원자적 연산을 수행할 수 있기 때문이라고 생각한다. 내장된 atomic 패키지의 AddInt.. 함수를 사용하여 testWithIntSafety() 처럼 사용한다면 안전하게 공유된 자원을 업데이트할 수 있다.



func main() {  
   // testWriteWithMap() // 에러  
   testReadWithMap()     // 안 에러
}  

func testWriteWithMap() {  
   var wg sync.WaitGroup  
   m := make(map[int]int)  

   for i := 0; i < 1000; i++ {  
      wg.Add(1)  
      go func() {  
         defer wg.Done()  

         m[0] += 1  
      }()  
   }  

   wg.Wait()  
   fmt.Println(m[0])  
}  

func testReadWithMap() {  
   var wg sync.WaitGroup  
   m := map[int]int{0: 10, 1: 11}  

   for i := 0; i < 1000; i++ {  
      wg.Add(1)  
      i := i // 여기서 i 를 재할당 하는 이유는 나중에 설명하겠다~~  
      go func() {  
         defer wg.Done()  
         fmt.Println(m[i%2])  
      }()  
   }  

   wg.Wait()  
}

func testWithInt() {  
   var wg sync.WaitGroup  
   m := 0  

   for i := 0; i < 1000; i++ {  
      wg.Add(1)  
      go func() {  
         defer wg.Done()  

         m += 1  
      }()  
   }  
   wg.Wait()  
   fmt.Println(m)  
}

func testWithIntSafety() {  
   var wg sync.WaitGroup  
   m := int32(0)  

   for i := 0; i < 1000; i++ {  
      wg.Add(1)  
      go func() {  
         defer wg.Done()  

         atomic.AddInt32(&m, 1)  
      }()  
   }  
   wg.Wait()  
   fmt.Println(m)  
}



근데 사실 map도 이런식으로 뮤텍스를 직접 구현한다면 안전하게 사용할 수 있기는 하다

type mWithMutex struct {  
   m     map[int]int  
   mutex sync.Mutex  
}

func updateMapSafety() {  
   var wg sync.WaitGroup  
   var m = mWithMutex{  
      m:     make(map[int]int),  
      mutex: sync.Mutex{},  
   }  

   for i := 0; i < 1000; i++ {  
      wg.Add(1)  
      go func() {  
         defer wg.Done()  

         m.mutex.Lock()  
         m.m[0] += 1  
         m.mutex.Unlock()  
      }()  
   }  

   wg.Wait()  
   fmt.Println(m.m)  
}

결론

Integer와 같은 타입의 연산이나, map에 대한 연산 모두 동시에 여러 고루틴에서 발생하면 기대한 결과와 다른 결과를 얻을 가능성이 많다. 하지만 map에 대한 동시 연산은 언어 차원에서 런타임 에러를 발생시킨다. 그 이유는 아마 Go에서 자체적으로 제공하는 원자적 연산 기능이 없기 떄문이지 않을까 라고 생각한다.