LoginSignup
7
4

More than 3 years have passed since last update.

go.uber.org/atomic パッケージを使って、atomicな値を扱う。

Last updated at Posted at 2019-10-15

tr:dr

  • Go routineを使って並行処理する中で同じ値を扱うときにはsync/atomicパッケージなどを使い、atomicな処理にする必要がある。
  • Uberが sync/atomicのwrapperを作っていて便利そうだったので、試してみた。

Go routineを使ったカウントアップ

  • Go routineを使ったカウントアップのプログラムを書いてみます。syncパッケージを使って1000回のcountupを行うプログラムです。

package singlecpu

import (
    "sync"
)

func count() int64 {

    var count int64
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            count++
            wg.Done()
        }()
    }
    wg.Wait()
    return count
}
  • テストコードを書いて実行してみます。
package singlecpu

import (
    "fmt"
    "runtime"
    "testing"
)

func TestCount(t *testing.T) {
    tests := []struct {
        expect     int64
    }{
        {
            expect:     1000,
        },
    }
    for _, test := range tests {
        t.Run(fmt.Sprintf("expect: %d", test.expect), func(t *testing.T) {
            actual := count()
            if actual != test.expect {
                t.Errorf("Assert error failed actual: %d expext: %d GOMAXPROCS: %d", actual, test.expect, runtime.GOMAXPROCS(0))
            }
        })
    }

}

go test ./...
ok      github.com/yutachaos/go-practice/atomic 0.205s
  • 大丈夫ですね。

GOMAXPROCS

  • GOではGOMAXPROCSで同時に利用するCPUの変更をすることが出来ます。今度はこちらを変更しつつtestを実行してみます。
    • テストコード内の runtime.GOMAXPROCS(test.GOMAXPROCS) を使ってプログラム内で変更しつつ実行。
package multicpu

import (
    "fmt"
    "runtime"
    "testing"
)

func TestCount(t *testing.T) {
    tests := []struct {
        GOMAXPROCS int
        expect     int64
    }{
        {
            GOMAXPROCS: 1,
            expect:     1000,
        },
        {
            GOMAXPROCS: 2,
            expect:     1000,
        },
        {
            GOMAXPROCS: 3,
            expect:     1000,
        },
        {
            GOMAXPROCS: 4,
            expect:     1000,
        },
    }
    for _, test := range tests {
        t.Run(fmt.Sprintf("GOMAXPROCS: %d", test.GOMAXPROCS), func(t *testing.T) {
            runtime.GOMAXPROCS(test.GOMAXPROCS)
            actual := count()
            if actual != test.expect {
                t.Errorf("Assert error failed actual: %d expext: %d GOMAXPROCS: %d", actual, test.expect, runtime.GOMAXPROCS(0))
            }
        })
    }

}
  • 実行結果
go test ./...
--- FAIL: TestCount (0.00s)
    --- FAIL: TestCount/GOMAXPROCS:_2 (0.00s)
        main_test.go:36: Assert error failed actual: 965 expext: 1000 GOMAXPROCS: 2
    --- FAIL: TestCount/GOMAXPROCS:_3 (0.00s)
        main_test.go:36: Assert error failed actual: 865 expext: 1000 GOMAXPROCS: 3
    --- FAIL: TestCount/GOMAXPROCS:_4 (0.00s)
        main_test.go:36: Assert error failed actual: 889 expext: 1000 GOMAXPROCS: 4
FAIL
FAIL    github.com/yutachaos/go-practice/atomic/multicpu    0.077s
FAIL
  • CPUが1個のときは大丈夫ですが、2個以上のときに失敗しました。
  • 細かい説明はここでは省略しますが、countの演算がatomicに行われないためにこのような事象が発生します。

-race オプション

  • go test-race オプションを付けて実行することで、このようなrace conditionが発生する可能性があるコードに警告を出すことが出来ます。

go test ./... -race
==================
WARNING: DATA RACE
Read at 0x00c00001a188 by goroutine 11:
  github.com/yutachaos/go-practice/atomic/multicpu_invalid.count.func1()
      /Users/yutachaos/src/github.com/yutachaos/go-practice/atomic/multicpu_invalid/count.go:14 +0x38

Previous write at 0x00c00001a188 by goroutine 150:
  github.com/yutachaos/go-practice/atomic/multicpu_invalid.count.func1()
      /Users/yutachaos/src/github.com/yutachaos/go-practice/atomic/multicpu_invalid/count.go:14 +0x4e

Goroutine 11 (running) created at:
  github.com/yutachaos/go-practice/atomic/multicpu_invalid.count()
      /Users/yutachaos/src/github.com/yutachaos/go-practice/atomic/multicpu_invalid/count.go:13 +0xe4
  github.com/yutachaos/go-practice/atomic/multicpu_invalid.TestCount.func1()
      /Users/yutachaos/src/github.com/yutachaos/go-practice/atomic/multicpu_invalid/count_test.go:34 +0x71
  testing.tRunner()
      /usr/local/Cellar/go/1.13.1/libexec/src/testing/testing.go:909 +0x199

Goroutine 150 (finished) created at:
  github.com/yutachaos/go-practice/atomic/multicpu_invalid.count()
      /Users/yutachaos/src/github.com/yutachaos/go-practice/atomic/multicpu_invalid/count.go:13 +0xe4
  github.com/yutachaos/go-practice/atomic/multicpu_invalid.TestCount.func1()
      /Users/yutachaos/src/github.com/yutachaos/go-practice/atomic/multicpu_invalid/count_test.go:34 +0x71
  testing.tRunner()
      /usr/local/Cellar/go/1.13.1/libexec/src/testing/testing.go:909 +0x199
==================
--- FAIL: TestCount (0.24s)
    --- FAIL: TestCount/GOMAXPROCS:_1 (0.10s)
        testing.go:853: race detected during execution of test
    --- FAIL: TestCount/GOMAXPROCS:_3 (0.05s)
        count_test.go:36: Assert error failed actual: 999 expext: 1000 GOMAXPROCS: 3
    --- FAIL: TestCount/GOMAXPROCS:_4 (0.05s)
        count_test.go:36: Assert error failed actual: 998 expext: 1000 GOMAXPROCS: 4
    testing.go:853: race detected during execution of test
FAIL
FAIL    github.com/yutachaos/go-practice/atomic/multicpu_invalid    0.505s
FAIL

sync/atomic

  • sync/atomicを利用することで、Golangで複数のgo routineから変数がアクセスされる際にatomicな計算を提供することができます。
package multicpu_invalid

import (
    "sync"
    "sync/atomic"
)

func count() int64 {

    var count int64
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            atomic.AddInt64(&count,1)
            wg.Done()
        }()
    }
    wg.Wait()
    return atomic.LoadInt64(&count)
}
  • テスト再実行

go test ./... -race
ok      github.com/yutachaos/go-practice/atomic/multicpu    1.526s
  • -race オプションを付けて実行していますが、今回はwarningも出ずに成功しています。

sync/atomicの問題点

  • atomic.AddInt64を利用することで、atomicな計算をすることができるのですが問題点があります。

忘れやすい

  • 各計算を行う際に扱う値を関数にセットしないといけないので、設定漏れなどが起きやすい。

Int32,Int64しか対応していない。

  • Int32,Int64しか対応していないので、Int32を利用した値をBoolで扱うなどのパターン、uintにしたいなどでそのまま扱いづらい。

go.uber.org/atomic

package multicpu_uber_atomic

import (
    "go.uber.org/atomic"
    "sync"
)

func count() int64 {

    count := atomic.NewInt64(0)
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            count.Add(1)
            wg.Done()
        }()
    }
    wg.Wait()
    return count.Load()
}
  • atomicに設定した値をmethod経由でアクセスしないといけないため、設定漏れが起きづらい!

  • 実行結果

go test ./...
ok      github.com/yutachaos/go-practice/atomic/multicpu_uber_atomic    0.533s

おわりに

余談

  • 最初はGo playgroundを使って、コード実行をしようとしたら、playground上の実行環境はCPU一つで普通に成功してしまった。

参考資料

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4