5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

スタンバイAdvent Calendar 2023

Day 4

「無害」なdata raceについて

Last updated at Posted at 2023-12-03

概要

Goの「無害」なdata raceについてはたしてそれは実際には無害なのか、避けるべきなのか例を交えてまとめている

結論

「無害」なdata raceも避けるべき

そもそもdata raceとは?

二つ以上のスレッドが同じ変数(メモリ)に対して同時にアクセスし、少なくとも一つのスレッドが書き込みの場合に発生する状態のこと

参考:https://ja.wikipedia.org/wiki/%E3%83%87%E3%83%BC%E3%82%BF%E7%AB%B6%E5%90%88

data raceの例

典型的な例で言うと、以下のように二つのgroutineからcount変数に対してアクセスする場合、

以下は0,1,2,3,4,5,,と表示されるわけではない

おそらく(タイミングによって変わる)0,0,0,0,1,,1,1,1,1みたいに表示される

package main

import (
	"fmt"
)

func main() {

	var count int
	ch := make(chan bool)

	go func() {
		for {
			count = count + 1
			fmt.Println(count)
		}
	}()

	go func() {
		for {
			fmt.Println(count)
		}
	}()

	<-ch

}

output

0
0
1
1
1
1
1
1
1

count変数を正確にひとつずつインクリメントしたい場合にはこの挙動はバグである

これが典型的な有害なdata raceである

これを解決するには、Mutex等を使ってcount変数へのアクセスを一つにlockする必要がある(ここでは解説しない)

参考
https://go.dev/tour/concurrency/9

無害なdata raceとは

では、無害なdata raceとはなんなのか?

countの値が100%順番を保っていないでもOKという時に、それは「無害」なdata raceという

先ほどと同じコード例

package main

import (
	"fmt"
)

func main() {

	var count int
	ch := make(chan bool)

	go func() {
		for {
			count = count + 1
			fmt.Println(count)
		}
	}()

	go func() {
		for {

			fmt.Println(count)
		}
	}()

	<-ch

}

では、「無害」なdata raceは放っておいても良いのか?

それとも避けるべきなのか?

Goでは、hashmapのdata raceを避けるために、data raceが検出されるとデフォルトでパニックを行うようになっている

文字列やHashMapのような複雑なデータ構造に対するdata raceは言うまでもなく、危険である(プログラムがクラッシュする可能性がある)>

GoではHashMapに対するdata raceは実行時にパニックを起こすようになっている

↓これを実行すると

package main

import "fmt"

func main() {
	ch := make(chan bool)
	hashMap := map[string]string{"a": "a"}

	go func() {
		for {
			hashMap["a"] = "aa"
		}
	}()

	go func() {
		for {
			fmt.Println(hashMap["a"])
		}

	}()

	<-ch

}

↓のようにパニックを引き起こす

fatal error: concurrent map read and map write

goroutine 7 [running]:
main.main.func2()
	/tmp/sandbox504299743/prog.go:19 +0x35
created by main.main in goroutine 1
	/tmp/sandbox504299743/prog.go:17 +0xf6

goroutine 1 [chan receive]:
main.main()
	/tmp/sandbox504299743/prog.go:24 +0x105

goroutine 6 [runnable]:
main.main.func1()
	/tmp/sandbox504299743/prog.go:13 +0x45
created by main.main in goroutine 1
	/tmp/sandbox504299743/prog.go:11 +0xb6

Program exited.

改めて結論

「無害」なdata raceも避けるべき

参考:Benign Data Races: What Could Possibly Go Wrong?

Goのrace detector作者による「無害な」data raceは何を引き起こすのかをまとめた資料

なぜなら

著者は以下の理由を挙げている

var count int

count++
  1. Goにおける↑のようなコードは未定義の動作である
    未定義の動作とは実行時に事実上どんな動作でも起こり得ると言うことを指している

  2. atomicやmutexを利用することで、data raceが発生しているのかどうかがコードで明確になる

  3. このような無害なdata raceを残しておくことで、data race detactorが検知する他の重要なdata raceを見逃してしまう可能性がある

「無害」なdata raceがどのようにして悪影響を及ぼす可能性があるのか例

前項の未定義の動作とは、どのような動作が考え得るのか

↓この場合、goroutine1でstop=trueになる前に、goroutine2が終了する可能性がある

var stop bool;

// goroutine1
go func(){
  ...
	stop = true
}()

// goroutine2
go func(){
	for {
		if stop {
			return
		}
		...
	}
}()

コンパイラによっては、race freeである前提で動作をする

変数stopが定義されるとそのメモリ領域を一時的なデータを格納しておく利用をする場合がある

その際に、stopに予期せぬデータが入っている状態でgoroutine2がstop変数にアクセスすると、予期せぬタイミングでgoroutine2が終了する

また、たとえば一時的なデータが非常に危険な関数のポインタとすると、予期せぬタイミングでその関数が実行されることになり、非常に危険な状態!

Go公式ドキュメントにも記載

Goの公式ドキュメントを読むと↓以下のモットーがある

Do not communicate by sharing memory; instead, share memory by communicating.

メモリをシェアすることでgoroutine間でコミュニケーションするのではなく、コミュニケーションをすることでメモリをシェアしよう!

参考

5
0
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
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?