sync/atomic 패키지를 사용하여 안전하게 수치 계산하기

8535 단어 Go
gooroutine에서 여러 개의 핵을 사용하여 병행 처리를 할 때 변수의 처리에 주의해야 한다.
여기에 여러 개의 Gooroutine이 한 개의 수치만 10000번 계수하는 실제 장치를 비교합니다.

TL;DR


수치의 계수를 안전하게 진행하기 위해 sync/atomic 패키지의 atomic.AddUint32() 등 함수를 사용합니다.

기준 환경


다음과 같은 환경에서 대략적인 측정을 진행한다.
CPU는 4코어입니다.

비교할 코드


++ 연산자를 고려할 필요가 없습니다

func useIncrementOperator() uint32 {
    var cnt uint32
    var wg sync.WaitGroup

    for i := 0; i < times; i++ {
        wg.Add(1)
        go func() {
            cnt++
            wg.Done()
        }()
    }

    wg.Wait()

    return cnt
}

sync/atomic 포장된 atomic.AddUint 32 사용()

func useAtomicAddUint32() uint32 {
    var cnt uint32
    var wg sync.WaitGroup

    for i := 0; i < times; i++ {
        wg.Add(1)
        go func() {
            atomic.AddUint32(&cnt, 1)
            wg.Done()
        }()
    }

    wg.Wait()

    return cnt
}

sync 포장된 sync.Mutex 사용

func useSyncMutexLock() uint32 {
    var cnt uint32
    var wg sync.WaitGroup
    mu := new(sync.Mutex)

    for i := 0; i < times; i++ {
        wg.Add(1)
        go func() {
            mu.Lock()
            defer mu.Unlock()
            cnt++
            wg.Done()
        }()
    }

    wg.Wait()

    return cnt
}

실행하다


그럼 이 코드를 실제로 실행해서 결과를 비교해 봅시다.
코드 전체가 여기.에 있습니다.
$ go run main.go
GOMAXPROCS: 1
useIncrementOperator(): 10000
useAtomicAddUint32(): 10000
useSyncMutexLock(): 10000
하나하나가 모두 10000에 이르렀다.
언뜻 보기에는 별 문제가 없는 것 같지만 GOMAXPROCS를 설정해서 여러 개의 핵심을 사용해 보겠습니다.
$ GOMAXPROCS=4 go run main.go
GOMAXPROCS: 4
useIncrementOperator(): 9637
useAtomicAddUint32(): 10000
useSyncMutexLock(): 10000
간단히++ 연산자 사용의 실현은 일치하지 않는다.
병렬 처리를 할 때++는 연산자로 가산점을 할 수 없다는 것을 안다.
이 질문-race도 옵션을 사용하여 확인할 수 있습니다.
$ go run -race main.go
GOMAXPROCS: 1
==================
WARNING: DATA RACE
Read by goroutine 6:
  main.func·001()
      /Users/yuya/src/github.com/yuya-takeyama/go-practice/sync/counter/main.go:19 +0x43

Previous write by goroutine 5:
  main.func·001()
      /Users/yuya/src/github.com/yuya-takeyama/go-practice/sync/counter/main.go:19 +0x57

Goroutine 6 (running) created at:
  main.useIncrementOperator()
      /Users/yuya/src/github.com/yuya-takeyama/go-practice/sync/counter/main.go:21 +0x14d
  main.main()
      /Users/yuya/src/github.com/yuya-takeyama/go-practice/sync/counter/main.go:69 +0x12a

Goroutine 5 (finished) created at:
  main.useIncrementOperator()
      /Users/yuya/src/github.com/yuya-takeyama/go-practice/sync/counter/main.go:21 +0x14d
  main.main()
      /Users/yuya/src/github.com/yuya-takeyama/go-practice/sync/counter/main.go:69 +0x12a
==================
useIncrementOperator(): 10000
useAtomicAddUint32(): 10000
useSyncMutexLock(): 10000
Found 1 data race(s)
exit status 66

벤치 표시를 해볼게요.


다음은 atomic.AddUint32()sync.Mutex 중 어느 것이 더 빠른지 비교해 봅시다.
이것도 전체 코드여기.입니다.
$ GOMAXPROCS=1 go test -bench .
testing: warning: no tests to run
PASS
BenchmarkUseIncrementOperator        200           9204798 ns/op
BenchmarkUseAtomicAddUint64          200           9354682 ns/op
BenchmarkUseSyncMutexLock            100          12104668 ns/op
ok      github.com/yuya-takeyama/go-practice/sync/counter       6.453s
GOMAXPROCS=1 상태에서 사용하는 방법sync.Mutex은 사용하는 방법atomic.AddUint32()보다 1.3배 빠르다.
다음GOMAXPROCS=4 실행해 봅시다.
$ GOMAXPROCS=4 go test -bench .
testing: warning: no tests to run
PASS
BenchmarkUseIncrementOperator-4      300           4242947 ns/op
BenchmarkUseAtomicAddUint64-4        300           4207403 ns/op
BenchmarkUseSyncMutexLock-4          200           6745847 ns/op
ok      github.com/yuya-takeyama/go-practice/sync/counter       5.496s
물론 시행 시간이 빨라졌지만 1.3배였던 시행 시간 차이는 1.6배에 달했다.
병렬도가 높을수록 록의 대기 시간은 길어진다.

좋은 웹페이지 즐겨찾기