ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Go 언어의 포인터와 atomic.Value의 이해
    Go 2024. 2. 14. 19:25

     

    ** 목 차 **

    1. atomic.Value 소개
    2. Go 언어의 포인터와 interface{}
    3. atomic.Value의 구현
      • Value 구조체 선언
      • ifaceWords 구조체 선언
      • 값 읽기 (Load 함수)
      • 값 추가 (Store 함수)
    4. atomic 패키지의 사용과 안전한 구현

    Go 언어의 포인터에 대한 이야기를 atomic.Value의 구현을 통해 살펴보겠습니다.

     

    atomic.Value는 무엇일까요? 이것은 Go 1.4부터 추가된 기능입니다.

     

    Go의 공식 문서에서는 "메모리를 공유하여 통신하지 마라."라는 말이 자주 등장합니다.

     

    그러나 여러 Goroutine에서 하나의 변수를 참조하거나 업데이트하는 것은 표준 패키지를 살펴보면 꽤 자주 나타나는 패턴입니다.

     

    이때 sync 패키지 등을 사용하여 값의 race condition을 해결합니다.

     

    공유하는 변수에 대한 변경 처리(예: map에 새 값을 추가하는 등)를 더 atomic한 형태로 제공하는 것이 바로 atomic.Value입니다.

     

    atomic.Value의 사용 방법은 아래 링크를 참조하십시오.

     

    atomic 패키지 - sync/atomic - Go 패키지 http://golang.org

     

    그럼 왜 이런 패키지를 사용해야 하는지, 내부 구현을 살펴보면서 생각해 보겠습니다.

     

    package atomic
    
    import (
        "unsafe"
    )
    
    type Value struct {
        v interface{}
    }

     

    이 부분은 일반적인 선언입니다.

     

    // interface{}의 내부적 표현, 형태와 값 각각의 포인터를 가짐
    type ifaceWords struct {
        typ unsafe.Pointer
        data unsafe.Pointer
    }

     

    위를 보면, 형태와 값을 각각 다른 포인터로 보관하고 있습니다.

     

    Go의 interface{}는 두 개의 word를 하나는 형태, 하나는 가리키는 값으로 따로 보관하고 있기 때문에, 이런 형태가 필요합니다.

     

    참조: InterfaceSlice https://github.com/golang/go/wiki/InterfaceSlice

    // 값 읽기 
    func (v *Value) Load() (x interface{}) {
        vp := (*ifaceWords)(unsafe.Pointer(v))
        typ := LoadPointer(&vp.typ)
        if typ == nil || uintptr(typ) == ^uintptr(0) {
            // 형의 포인터가 정의되어 있지 않은 경우, 첫 데이터 추가가 완료되지 않았다고 간주한다.
            return nil
        }
        data := LoadPointer(&vp.data)
        반환 값 x의 포인터에 대해 interface의 형 정보와 값 정보의 포인터를 설정한다
        xp := (*ifaceWords)(unsafe.Pointer(&x))
        xp.typ = typ
        xp.data = data
        return
    }

     

    위에서는 atomic.LoadPointer가 많이 사용되고 있습니다.

     

    데이터를 atomic.Value가 보유하고 있는 경우, 값을 포인터로 atomic하게 읽어들여 반환 값에 전달합니다.

     

    // 값 추가
    func (v *Value) Store(x interface{}) {
        if x == nil {
            panic("sync/atomic: store of nil value into Value")
        }
        vp := (*ifaceWords)(unsafe.Pointer(v))
        xp := (*ifaceWords)(unsafe.Pointer(&x))
        for {
            typ := LoadPointer(&vp.typ)
            if typ == nil {
                runtime_procPin() // 이를 호출함으로써 컨텍스트 스위치를 중지하고, 다른 goroutine 등을 대기시킨다
                // typ의 포인터에 초기 쓰기 중임을 할당한다
                if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
                    runtime_procUnpin()
                    continue
                }
                // 첫 쓰기 처리를 포인터에 대해 실시한다.
                StorePointer(&vp.data, xp.data)
                StorePointer(&vp.typ, xp.typ)
                runtime_procUnpin()
                return
            }
            if uintptr(typ) == ^uintptr(0) {
                // typ의 포인터가 위의 쓰기 처리 중인 flag와 같은 값이면 처음으로 돌아간다
                continue
            }
            // 다른 형을 보고 있는 경우는 오류
            if typ != xp.typ {
                panic("sync/atomic: store of inconsistently typed value into Value")
            }
            StorePointer(&vp.data, xp.data)
            return
        }
    }

     

    쓰기 시에는, 컨텍스트 스위치를 중지하고, 초기 쓰기 중임을 ^uintptr(0)이라는 값으로 플래그로서 typ에 전달하고, 그 후 형태와 값 각각의 포인터를 교체합니다.

    func runtime_procPin()
    func runtime_procUnpin()

     

    이를 선언해 두면, 런타임 측에서 컨텍스트 스위치를 중지하거나 재개할 수 있습니다.

     

    이런 자료 등을 참조하면, interface의 pointer 교체 처리는 word 크기를 초과하는 non-atomic 처리이므로, 컨텍스트 스위치를 중지하면서 값을 바꾸고, 읽기 중임을 명시하는 등, 단순한 포인터의 교체 처리를 하려고 해도, 여러 가지 주의가 필요한 것 같습니다.

     

    마지막으로 Global 변수를 여러 곳에서 둘러보는 것을 자신이 안전하게 구현하려고 하면, unsafe 패키지로 번거로운 구현을 해야 하므로 atomic 패키지를 잘 사용하거나, channel에 의존합시다.

     

Designed by Tistory.