結論から言うと
rand.Sourceを使いまわすとpanicが起きます。
サーバーサイドで行うと、永遠に乱数パニックです。
しかも、本番環境にリリースするまでわからないため、うっかり実装すると凶悪バグの出来上がりです。
はじめに
インスタンスを再生成するコストを抑えるために使いまわそうと、誰でも一度は考えますよね。
スレッドセーフでどうかと言う概念を知らないと、サーバーをGoで立てたときに後悔することになります。
今回はrand.Sourceを使いまわすとどうなるのかを実験していきます。
実験
強制的にエラーを引き起こすために並列処理でrand.Sourceを呼ぶ実装を雑に作りました。
環境
$go version
go version go1.13 darwin/amd64
実験用ソースコード
package main
import (
"fmt"
"math/rand"
"sync"
)
var randSource = NewRandSource()
// rand.Sourceを使いまわすとどうなるかの実験コード
func main() {
ch := make(chan int, 100)
wg := sync.WaitGroup{}
for i := 0; i < 1000; i++ {
ch <- 1
wg.Add(1)
go func() {
calcRnad()
<-ch
wg.Done()
}()
fmt.Println("for count =", i)
}
wg.Wait()
}
func calcRnad() {
for i := 0; i < 10000; i++ {
randSource.Intn(1000) // インスタンスを使いまわす場合
// NewRandSource().Intn(1000)// インスタンスを毎回生成する記述
}
}
// NewRandSource 新しいインスタンスを作る
func NewRandSource() *rand.Rand {
return rand.New(rand.NewSource(time.Now().UnixNano()))
}
簡単に説明すると100のスレッドを作って、10000回randを呼び出す処理を書いただけです。
動作チェックのためにエラーにならないコードをコメントしています。
実行結果
下記の通り12スレッド目で、panic:runtimeエラーが起きました。
go run main.go
for count = 0
for count = 1
for count = 2
for count = 3
for count = 4
for count = 5
for count = 6
for count = 7
for count = 8
for count = 9
for count = 10
for count = 11
for count = 12
panic: runtime error: index out of range [-1]
goroutine 22 [running]:
math/rand.(*rngSource).Uint64(...)
/usr/local/Cellar/go/1.13/libexec/src/math/rand/rng.go:249
math/rand.(*rngSource).Int63(0xc000089500, 0xf886e3da440ba38)
/usr/local/Cellar/go/1.13/libexec/src/math/rand/rng.go:234 +0x93
math/rand.(*Rand).Int63(...)
/usr/local/Cellar/go/1.13/libexec/src/math/rand/rand.go:85
math/rand.(*Rand).Int31(...)
/usr/local/Cellar/go/1.13/libexec/src/math/rand/rand.go:99
math/rand.(*Rand).Int31n(0xc0000641b0, 0x3e8, 0x165)
/usr/local/Cellar/go/1.13/libexec/src/math/rand/rand.go:134 +0x5f
math/rand.(*Rand).Intn(0xc0000641b0, 0x3e8, 0x165)
/usr/local/Cellar/go/1.13/libexec/src/math/rand/rand.go:172 +0x45
main.calcRnad()
/playground/go/src/rand/main.go:32 +0x3f
main.main.func1(0xc00008c000, 0xc000016054)
/playground/go/src/rand/main.go:21 +0x22
created by main.main
/playground/go/src/rand/main.go:20 +0xc7
exit status 2
painicとなった箇所の実装を眺めてみる
対象ファイルの237行目から
// Uint64 returns a non-negative pseudo-random 64-bit integer as an uint64.
func (rng *rngSource) Uint64() uint64 {
rng.tap--
if rng.tap < 0 {
rng.tap += rngLen
}
rng.feed--
if rng.feed < 0 {
rng.feed += rngLen
}
x := rng.vec[rng.feed] + rng.vec[rng.tap]
rng.vec[rng.feed] = x
return uint64(x)
}
rng.tapとrng.feedが負の値にならないように実装しているのがわかります。
しかし、並列処理の場合は
1. スレッドA処理:rng.tap < 0のチェック
2. スレッドB処理:rng.tap--で負の値になる
3. スレッドA処理:rng.vec[rng.tap]が負のindexを見てpanicになる
となる可能性が確率的に存在します
おわりに
並列で使わなければいいんです。並列で。
参考
なおissuesでも議論されてる模様
https://github.com/golang/go/issues/21393