概要
Goは、非同期処理が比較的簡単に書けるので、ちょちょいと書いたりしているとやってしまいがちなミスかもしれません。
Goも他の言語の場合と同じく、同じ値に対して複数の処理がアクセスしていて、そのどれか一つでも書き込みの場合は同期化が必要です。
んで、この話は当然ながらスライスにも適用されます。正しくは、スライスの各要素ではなくて、スライスヘッダの方です。
Goroutineの中で、 append() を使ってスライスを処理している場合、正しく処理していないとデータ競合が発生します。
サンプル
Taskfile.yml
version: '3' tasks: fmt: cmds: - go fmt ./... vet: cmds: - go vet ./... run: cmds: - cmd: for i in {1..10} ; do go run race/main.go; done silent: true run-notrace: cmds: - cmd: for i in {1..10} ; do go run -race notrace/main.go; done silent: true run-notrace2: cmds: - cmd: for i in {1..10} ; do go run -race notrace2/main.go; done silent: true run-with-raceoption: cmds: - cmd: go run -race race/main.go ignore_error: true
データ競合が発生する版
// スライス操作 (スライスヘッダの書き換え)はスレッドセーフでは無いというのを示すサンプルです。 // 本サンプルはデータ競合が発生しています。 // // REFERENCES: // - https://stackoverflow.com/questions/44152988/append-not-thread-safe // - https://stackoverflow.com/questions/49879322/can-i-concurrently-write-different-slice-elements package main import ( "flag" "fmt" "strconv" "sync" ) type data struct { value string } func (me data) String() string { return me.value } func main() { var ( verbose = flag.Bool("verbose", false, "verbose output") ) flag.Parse() var ( src = make([]data, 100) dst = make([]data, 0) ) for i := 0; i < len(src); i++ { src[i].value = strconv.Itoa(i) } wg := sync.WaitGroup{} for _, v := range src { wg.Add(1) go func(v data) { defer wg.Done() var tmp data tmp.value = v.value dst = append(dst, tmp) }(v) } wg.Wait() fmt.Printf("src-len=%d\tdst-len=%d\n", len(src), len(dst)) if *verbose { fmt.Println("=========== SRC ===========") for _, v := range src { fmt.Println(v) } fmt.Println("=========== DST ===========") for _, v := range dst { fmt.Println(v) } } }
スライスに対して append() しているところで、同期させていないので、上はデータ競合が発生します。
実行すると以下のようになります。
$ task run src-len=100 dst-len=92 src-len=100 dst-len=82 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=99 src-len=100 dst-len=70 src-len=100 dst-len=56 src-len=100 dst-len=69 src-len=100 dst-len=66 src-len=100 dst-len=87
結果が合いません。つまりデータ競合が発生しているということになります。
-race
オプションをつけるとちゃんと報告されます。
$ task run-with-raceoption task: [run-with-raceoption] go run -race race/main.go ================== WARNING: DATA RACE Read at 0x00c0001ae018 by goroutine 8: main.main.func1() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:48 +0xb9 main.main.func2() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:49 +0x58 Previous write at 0x00c0001ae018 by goroutine 7: main.main.func1() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:48 +0x16a main.main.func2() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:49 +0x58 Goroutine 8 (running) created at: main.main() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:42 +0x3f3 Goroutine 7 (finished) created at: main.main() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:42 +0x3f3 ================== ================== WARNING: DATA RACE Read at 0x00c000012040 by goroutine 9: runtime.growslice() /home/gitpod/go/src/runtime/slice.go:166 +0x0 main.main.func1() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:48 +0xf1 main.main.func2() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:49 +0x58 Previous write at 0x00c000012040 by goroutine 7: main.main.func1() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:48 +0x11e main.main.func2() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:49 +0x58 Goroutine 9 (running) created at: main.main() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:42 +0x3f3 Goroutine 7 (finished) created at: main.main() /workspace/try-golang/examples/singleapp/slice_is_not_threadsafe/race/main.go:42 +0x3f3 ================== src-len=100 dst-len=93 Found 2 data race(s) exit status 66
データ競合が発生しないようにする(1)
保護するために、 sync.Mutex を利用している版
// スライス操作 (スライスヘッダの書き換え)はスレッドセーフでは無いというのを示すサンプルです。 // 本サンプルはデータ競合が発生しません。 // // REFERENCES: // - https://stackoverflow.com/questions/44152988/append-not-thread-safe // - https://stackoverflow.com/questions/49879322/can-i-concurrently-write-different-slice-elements package main import ( "flag" "fmt" "strconv" "sync" ) type data struct { value string } func (me data) String() string { return me.value } func main() { var ( verbose = flag.Bool("verbose", false, "verbose output") ) flag.Parse() var ( mu = sync.Mutex{} src = make([]data, 100) dst = make([]data, 0) ) for i := 0; i < len(src); i++ { src[i].value = strconv.Itoa(i) } wg := sync.WaitGroup{} for _, v := range src { wg.Add(1) go func(v data) { defer wg.Done() var tmp data tmp.value = v.value mu.Lock() dst = append(dst, tmp) mu.Unlock() }(v) } wg.Wait() fmt.Printf("src-len=%d\tdst-len=%d\n", len(src), len(dst)) if *verbose { fmt.Println("=========== SRC ===========") for _, v := range src { fmt.Println(v) } fmt.Println("=========== DST ===========") for _, v := range dst { fmt.Println(v) } } }
ちゃんと同期化させているので、こちらはデータ競合が発生しません。
$ task run-notrace src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100
データ競合が発生しないようにする(2)
同期化が必要かどうかのルールは以下のstackoverflowのページにわかりやすく記載されています。非同期処理を作る場合にとても大事なルールです。
The rule is simple: if multiple goroutines access a variable concurrently, and at least one of the accesses is a write, then synchronization is required.
ルールは簡単で、複数のゴルーチンが同時に変数にアクセスし、そのうちの少なくとも1つが書き込みである場合、同期が必要であるというものです。
なので、各々のGoroutineがそれぞれ独立したデータにアクセスしている場合は同期化は必要ないということになります。
// スライス操作 (スライスヘッダの書き換え)はスレッドセーフでは無いというのを示すサンプルです。 // 本サンプルはデータ競合が発生しません。 // // REFERENCES: // - https://stackoverflow.com/questions/44152988/append-not-thread-safe // - https://stackoverflow.com/questions/49879322/can-i-concurrently-write-different-slice-elements package main import ( "flag" "fmt" "strconv" "sync" ) type data struct { value string } func (me data) String() string { return me.value } func main() { var ( verbose = flag.Bool("verbose", false, "verbose output") ) flag.Parse() var ( src = make([]data, 100) dst = make([]data, len(src)) ) for i := 0; i < len(src); i++ { src[i].value = strconv.Itoa(i) } wg := sync.WaitGroup{} for i, v := range src { wg.Add(1) go func(i int, v data) { defer wg.Done() var tmp data tmp.value = v.value dst[i] = tmp }(i, v) } wg.Wait() fmt.Printf("src-len=%d\tdst-len=%d\n", len(src), len(dst)) if *verbose { fmt.Println("=========== SRC ===========") for _, v := range src { fmt.Println(v) } fmt.Println("=========== DST ===========") for _, v := range dst { fmt.Println(v) } } }
sync.Mutexを使っていませんが、この場合はデータ競合が発生しません。
$ task run-notrace2 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100 src-len=100 dst-len=100
データ競合が発生しないようにする(3)
Goには、このような場合に同期化を考えなくてもデータのやり取りが出来る構造として channel が用意されています。
なので、上記の処理は channel を利用して処理すれば同然ながらデータ競合しません。サンプルは割愛。
参考情報
過去の記事については、以下のページからご参照下さい。
サンプルコードは、以下の場所で公開しています。