sync/atomic パッケージを使って数値を安全にカウントアップする

goroutine で複数コアを使った並列・並行処理を行う場合、変数の取り扱いに気をつける必要があります。
ここでは複数の goroutine でひとつの数値を 10000 回カウントアップをするだけの実装を複数比較します.

# TL;DR

数値のカウントアップを安全に行うには `sync/atomic` パッケージの `atomic.AddUint32()` 等の関数を使う。

# ベンチマーク環境

以下のような環境で雑に計測します。
CPU は 4 コアです。

![mac.png](https://qiita-image-store.s3.amazonaws.com/0/208/3ce88611-c138-9cb0-aa6d-d500386b1599.png)


# 比較するコード

## 何も考えずに ++ 演算子を使う

```go
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.AddUint32() を使う

```go
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 を使う

```go
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
}
```

# 実行してみる

では実際にこのコードを実行して結果を比較してみます。
コード全体は[こちら](https://github.com/yuya-takeyama/go-practice/tree/5e6cb84f7286e84cbc08ee1457aa3d88c73745c5/sync/counter)にあります。

```
$ go run main.go
GOMAXPROCS: 1
useIncrementOperator(): 10000
useAtomicAddUint32(): 10000
useSyncMutexLock(): 10000
```

どの実装も 10,000 になりました。
一見問題なさそうですが、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` どちらを使うのがより速いのかを比較します。
これもコード全体は[こちら](https://github.com/yuya-takeyama/go-practice/tree/5e6cb84f7286e84cbc08ee1457aa3d88c73745c5/sync/counter)にあります。

```
$ 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 倍にもなりました。
並列度が高いときほどロックによる待ち時間が長くなってしまうのだと考えられます。