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
- sync/atomicのwrapper
- atomicに扱う値を
NewXXX()
の形でstructでwrapperし、structとmethodを経由する形でAddやLoadを行う。 - uber/atomicはBool、uintの値をサポートしている。
- https://godoc.org/go.uber.org/atomic#NewBool
- 内部的には
atomic.LoadUint32(&b.v)
をwrapperしている。
-
go.uber.org/atomic
で書き直したversion
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.uber.org/atomic
パッケージはUberのGoのguidelineで紹介されていたも
ので、他の項目もGoを書く上では役立つものがたくさんあったので、興味を持った人は是非UberのGuideも読んでみるのもおすすめです。 - 今回の記事で利用したコードは下記においてあります。
余談
- 最初はGo playgroundを使って、コード実行をしようとしたら、playground上の実行環境はCPU一つで普通に成功してしまった。
参考資料
- The Go Memory Model
- atomicパッケージが必要な理由と使い方
- golang の sync パッケージの使い方
- Uber go Guide use go uber atomic