2
0

More than 3 years have passed since last update.

【Go】rand.Sourceを並列で使いまわすなんて何事だ

Last updated at Posted at 2019-09-16

結論から言うと

rand.Sourceを使いまわすとpanicが起きます。
サーバーサイドで行うと、永遠に乱数パニックです。
しかも、本番環境にリリースするまでわからないため、うっかり実装すると凶悪バグの出来上がりです。

はじめに

インスタンスを再生成するコストを抑えるために使いまわそうと、誰でも一度は考えますよね。
スレッドセーフでどうかと言う概念を知らないと、サーバーをGoで立てたときに後悔することになります。
今回はrand.Sourceを使いまわすとどうなるのかを実験していきます。

実験

強制的にエラーを引き起こすために並列処理でrand.Sourceを呼ぶ実装を雑に作りました。

環境

$go version
go version go1.13 darwin/amd64

実験用ソースコード

main.go
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行目から

math/rand/rng.go
// 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

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